Merge branch 'pyfa-org:master' into master

This commit is contained in:
StormDelay
2025-05-20 18:33:48 +02:00
committed by GitHub
791 changed files with 382527 additions and 28685 deletions

View File

@@ -69,7 +69,7 @@ class Ammo:
falloff = (mod.item.getAttribute('falloff') or 0) * \
(charge.getAttribute('fallofMultiplier') or 1)
for type_ in DmgTypes.names():
d = charge.getAttribute('%sDamage' % type_)
d = charge.getAttribute('%sDamage' % type_, default=0)
if d > 0:
damage += d
# Take optimal and falloff as range factor

View File

@@ -44,7 +44,7 @@ class EsiEndpoints(Enum):
"""
CHAR = "/v5/characters/{character_id}/"
CHAR_SKILLS = "/v4/characters/{character_id}/skills/"
CHAR_FITTINGS = "/v1/characters/{character_id}/fittings/"
CHAR_FITTINGS = "/v2/characters/{character_id}/fittings/"
CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/"
DYNAMIC_ITEM = "/v1/dogma/dynamic/items/{type_id}/{item_id}/"

View File

@@ -0,0 +1,19 @@
"""
Conversion pack for Equinox release
"""
CONVERSIONS = {
# Renamed items
"Imperial Navy Mjolnir Auto-Targeting Cruise Missile I": "Legion Mjolnir Auto-Targeting Cruise Missile",
"Caldari Navy Scourge Auto-Targeting Cruise Missile I": "Legion Scourge Auto-Targeting Cruise Missile",
"Federation Navy Inferno Auto-Targeting Cruise Missile I": "Legion Inferno Auto-Targeting Cruise Missile",
"Republic Fleet Nova Auto-Targeting Cruise Missile I": "Legion Nova Auto-Targeting Cruise Missile",
"Imperial Navy Mjolnir Auto-Targeting Heavy Missile I": "Legion Mjolnir Auto-Targeting Heavy Missile",
"Caldari Navy Scourge Auto-Targeting Heavy Missile I": "Legion Scourge Auto-Targeting Heavy Missile",
"Federation Navy Inferno Auto-Targeting Heavy Missile I": "Legion Inferno Auto-Targeting Heavy Missile",
"Republic Fleet Nova Auto-Targeting Heavy Missile I": "Legion Nova Auto-Targeting Heavy Missile",
"Imperial Navy Mjolnir Auto-Targeting Light Missile I": "Legion Mjolnir Auto-Targeting Light Missile",
"Caldari Navy Scourge Auto-Targeting Light Missile I": "Legion Scourge Auto-Targeting Light Missile",
"Federation Navy Inferno Auto-Targeting Light Missile I": "Legion Inferno Auto-Targeting Light Missile",
"Republic Fleet Nova Auto-Targeting Light Missile I": "Legion Nova Auto-Targeting Light Missile",
}

View File

@@ -0,0 +1,10 @@
"""
Actually renamed somewhere during summer, but updated only in september
"""
CONVERSIONS = {
# Renamed items
"Atgeir Explosive Disruptive Lance": "'Atgeir' Explosive Disruptive Lance",
"Steel Yari Kinetic Disruptive Lance": "'Steel Yari' Kinetic Disruptive Lance",
"Sarissa Thermal Disruptive Lance": "'Sarissa' Thermal Disruptive Lance",
}

View File

@@ -0,0 +1,24 @@
CONVERSIONS = {
# Renamed items
"Large Rudimentary Concussion Bomb I": "'Concussion' Compact Large Graviton Smartbomb",
"Small Rudimentary Concussion Bomb I": "'Concussion' Compact Small Graviton Smartbomb",
"Large 'Vehemence' Shockwave Charge": "'Vehemence' Compact Large EMP Smartbomb",
"Small 'Vehemence' Shockwave Charge": "'Vehemence' Compact Small EMP Smartbomb",
"Medium Rudimentary Concussion Bomb I": "'Concussion' Compact Medium Graviton Smartbomb",
"Medium 'Vehemence' Shockwave Charge": "'Vehemence' Compact Medium EMP Smartbomb",
"Small 'Notos' Explosive Charge I": "'Notos' Compact Small Proton Smartbomb",
"Medium 'Notos' Explosive Charge I": "'Notos' Compact Medium Proton Smartbomb",
"Large 'Notos' Explosive Charge I": "'Notos' Compact Large Proton Smartbomb",
"Small YF-12a Smartbomb": "'YF-12a' Compact Small Plasma Smartbomb",
"Medium YF-12a Smartbomb": "'YF-12a' Compact Medium Plasma Smartbomb",
"Large YF-12a Smartbomb": "'YF-12a' Compact Large Plasma Smartbomb",
"Small Degenerative Concussion Bomb I": "'Degenerative' Small Proton Smartbomb",
"Small Degenerative Concussion Bomb I Blueprint": "'Degenerative' Small Proton Smartbomb Blueprint",
"Medium Degenerative Concussion Bomb I": "'Dwindling' Medium Proton Smartbomb",
"Medium Degenerative Concussion Bomb I Blueprint": "'Dwindling' Medium Proton Smartbomb Blueprint",
"Large Degenerative Concussion Bomb I": "'Regressive' Large Proton Smartbomb",
"Large Degenerative Concussion Bomb I Blueprint": "'Regressive' Large Proton Smartbomb Blueprint",
"'Pike' Small EMP Smartbomb I": "'Pike' Small EMP Smartbomb",
"'Lance' Medium EMP Smartbomb I": "'Lance' Medium EMP Smartbomb",
"'Warhammer' Large EMP Smartbomb I": "'Warhammer' Large EMP Smartbomb",
}

View File

@@ -6,14 +6,14 @@ import time
import base64
import json
import config
import webbrowser
import re
import eos.db
from service.const import EsiLoginMethod, EsiSsoMode
from eos.saveddata.ssocharacter import SsoCharacter
from service.esiAccess import APIException, GenericSsoError
import gui.globalEvents as GE
from gui.ssoLogin import SsoLogin, SsoLoginServer
from gui.ssoLogin import SsoLogin
from service.server import StoppableHTTPServer, AuthHandler
from service.settings import EsiSettings
from service.esiAccess import EsiAccess
@@ -22,6 +22,7 @@ import gui.mainFrame
from requests import Session
pyfalog = Logger(__name__)
_t = wx.GetTranslation
class Esi(EsiAccess):
@@ -69,8 +70,8 @@ class Esi(EsiAccess):
chars = eos.db.getSsoCharacters(config.getClientSecret())
return chars
def getSsoCharacter(self, id):
char = eos.db.getSsoCharacter(id, config.getClientSecret())
def getSsoCharacter(self, id, server=None):
char = eos.db.getSsoCharacter(id, config.getClientSecret(), server)
eos.db.commit()
return char
@@ -101,15 +102,36 @@ class Esi(EsiAccess):
self.fittings_deleted.add(fittingID)
def login(self):
# always start the local server if user is using client details. Otherwise, start only if they choose to do so.
if self.settings.get('loginMode') == EsiLoginMethod.SERVER:
with gui.ssoLogin.SsoLoginServer(0) as dlg:
dlg.ShowModal()
else:
with gui.ssoLogin.SsoLogin() as dlg:
if dlg.ShowModal() == wx.ID_OK:
message = json.loads(base64.b64decode(dlg.ssoInfoCtrl.Value.strip()))
self.handleLogin(message)
start_server = self.settings.get('loginMode') == EsiLoginMethod.SERVER and self.server_base.supports_auto_login
with gui.ssoLogin.SsoLogin(self.server_base, start_server) as dlg:
if dlg.ShowModal() == wx.ID_OK:
from gui.esiFittings import ESIExceptionHandler
try:
if self.server_name == "Serenity":
s = re.search(r'(?<=code=)[a-zA-Z0-9\-_]*', dlg.ssoInfoCtrl.Value.strip())
if s:
# skip state verification and go directly through the auth code processing
self.handleLogin(s.group(0))
else:
pass
# todo: throw error
else:
self.handleServerRequest(json.loads(base64.b64decode(dlg.ssoInfoCtrl.Value.strip())))
except GenericSsoError as ex:
pyfalog.error(ex)
with wx.MessageDialog(
self.mainFrame,
str(ex),
_t("SSO Error"),
wx.OK | wx.ICON_ERROR
) as dlg:
dlg.ShowModal()
except APIException as ex:
pyfalog.error(ex)
ESIExceptionHandler(ex)
pass
def stopServer(self):
pyfalog.debug("Stopping Server")
@@ -127,24 +149,26 @@ class Esi(EsiAccess):
self.httpd = StoppableHTTPServer(('localhost', port), AuthHandler)
port = self.httpd.socket.getsockname()[1]
self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerLogin,))
self.serverThread = threading.Thread(target=self.httpd.serve, args=(self.handleServerRequest,))
self.serverThread.name = "SsoCallbackServer"
self.serverThread.daemon = True
self.serverThread.start()
return 'http://localhost:{}'.format(port)
def handleLogin(self, message):
auth_response, data = self.auth(message['code'])
def handleLogin(self, code):
auth_response, data = self.auth(code)
currentCharacter = self.getSsoCharacter(data['name'])
currentCharacter = self.getSsoCharacter(data['name'], self.server_base.name)
sub_split = data["sub"].split(":")
if (len(sub_split) != 3):
if len(sub_split) != 3:
raise GenericSsoError("JWT sub does not contain the expected data. Contents: %s" % data["sub"])
cid = sub_split[-1]
if currentCharacter is None:
currentCharacter = SsoCharacter(cid, data['name'], config.getClientSecret())
currentCharacter = SsoCharacter(cid, data['name'], config.getClientSecret(), self.server_base.name)
Esi.update_token(currentCharacter, auth_response)
@@ -153,7 +177,7 @@ class Esi(EsiAccess):
# get (endpoint, char, data?)
def handleServerLogin(self, message):
def handleServerRequest(self, message):
if not message:
raise GenericSsoError("Could not parse out querystring parameters.")
@@ -169,4 +193,4 @@ class Esi(EsiAccess):
pyfalog.debug("Handling SSO login with: {0}", message)
self.handleLogin(message)
self.handleLogin(message['code'])

View File

@@ -1,6 +1,7 @@
# noinspection PyPackageRequirements
from collections import namedtuple
import requests
from logbook import Logger
import uuid
import time
@@ -30,13 +31,6 @@ scopes = [
'esi-fittings.write_fittings.v1'
]
ApiBase = namedtuple('ApiBase', ['sso', 'esi'])
supported_servers = {
"Tranquility": ApiBase("login.eveonline.com", "esi.evetech.net"),
"Singularity": ApiBase("sisilogin.testeveonline.com", "esi.evetech.net"),
"Serenity": ApiBase("login.evepc.163.com", "esi.evepc.163.com")
}
class GenericSsoError(Exception):
""" Exception used for generic SSO errors that aren't directly related to an API call
"""
@@ -63,10 +57,11 @@ class APIException(Exception):
class EsiAccess:
server_meta = {}
def __init__(self):
self.settings = EsiSettings.getInstance()
self.server_base: ApiBase = supported_servers[self.settings.get("server")]
self.default_server_name = self.settings.get('server')
self.default_server_base = config.supported_servers[self.default_server_name]
# session request stuff
self._session = Session()
self._basicHeaders = {
@@ -78,23 +73,38 @@ class EsiAccess:
self._session.headers.update(self._basicHeaders)
self._session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat()
self.mem_cached_session = {}
# Set up cached session. This is only used for SSO meta data for now, but can be expanded to actually handle
# various ESI caching (using ETag, for example) in the future
cached_session = CachedSession(
self.cached_session = CachedSession(
os.path.join(config.savePath, config.ESI_CACHE),
backend="sqlite",
cache_control=True, # Use Cache-Control headers for expiration, if available
expire_after=timedelta(days=1), # Otherwise expire responses after one day
stale_if_error=True, # In case of request errors, use stale cache data if possible
)
cached_session.headers.update(self._basicHeaders)
cached_session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat()
self.cached_session.headers.update(self._basicHeaders)
self.cached_session.proxies = NetworkSettings.getInstance().getProxySettingsInRequestsFormat()
self.init(self.default_server_base)
def init(self, server_base):
self.server_base: config.ApiServer = server_base
self.server_name = self.server_base.name
try:
meta_call = self.cached_session.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso)
except:
# The http data of expire_after in evepc.163.com is -1
meta_call = requests.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso)
meta_call = cached_session.get("https://%s/.well-known/oauth-authorization-server" % self.server_base.sso)
meta_call.raise_for_status()
self.server_meta = meta_call.json()
jwks_call = cached_session.get(self.server_meta["jwks_uri"])
try:
jwks_call = self.cached_session.get(self.server_meta["jwks_uri"])
except:
jwks_call = requests.get(self.server_meta["jwks_uri"])
jwks_call.raise_for_status()
self.jwks = jwks_call.json()
@@ -116,7 +126,7 @@ class EsiAccess:
@property
def client_id(self):
return self.settings.get('clientID') or config.API_CLIENT_ID
return self.settings.get('clientID') or self.server_base.client_id
@staticmethod
def update_token(char, tokenResponse):
@@ -142,16 +152,25 @@ class EsiAccess:
'state': self.state
}
args = {
'response_type': 'code',
'redirect_uri': config.SSO_CALLBACK,
'client_id': self.client_id,
'scope': ' '.join(scopes),
'code_challenge': code_challenge,
'code_challenge_method': 'S256',
'state': base64.b64encode(bytes(json.dumps(state_arg), 'utf-8'))
}
if(self.server_name=="Serenity"):
args = {
'response_type': 'code',
'redirect_uri': self.server_base.callback,
'client_id': self.client_id,
'scope': ' '.join(scopes),
'state': 'hilltech',
'device_id': 'eims'
}
else:
args = {
'response_type': 'code',
'redirect_uri': self.server_base.callback,
'client_id': self.client_id,
'scope': ' '.join(scopes),
'code_challenge': code_challenge,
'code_challenge_method': 'S256',
'state': base64.b64encode(bytes(json.dumps(state_arg), 'utf-8'))
}
return '%s?%s' % (
self.oauth_authorize,
urlencode(args)
@@ -252,6 +271,11 @@ class EsiAccess:
"https://login.eveonline.com: {}".format(str(e)))
def _before_request(self, ssoChar):
if ssoChar:
self.init(config.supported_servers[ssoChar.server])
else:
self.init(self.default_server_base)
self._session.headers.clear()
self._session.headers.update(self._basicHeaders)
if ssoChar is None:
@@ -280,17 +304,17 @@ class EsiAccess:
def get(self, ssoChar, endpoint, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.get("{}{}".format(self.esi_url, endpoint)))
return self._after_request(self._session.get("{}{}?datasource={}".format(self.esi_url, endpoint, self.server_name.lower())))
def post(self, ssoChar, endpoint, json, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.post("{}{}".format(self.esi_url, endpoint), data=json))
return self._after_request(self._session.post("{}{}?datasource={}".format(self.esi_url, endpoint, self.server_name.lower()), data=json))
def delete(self, ssoChar, endpoint, **kwargs):
self._before_request(ssoChar)
endpoint = endpoint.format(**kwargs)
return self._after_request(self._session.delete("{}{}".format(self.esi_url, endpoint)))
return self._after_request(self._session.delete("{}{}?datasource={}".format(self.esi_url, endpoint, self.server_name.lower())))
# todo: move these off to another class which extends this one. This class should only handle the low level
# authentication and

View File

@@ -88,7 +88,7 @@ class Fit:
"enableGaugeAnimation": True,
"openFitInNew": False,
"priceSystem": "Jita",
"priceSource": "evemarketer",
"priceSource": "fuzzwork market",
"showShipBrowserTooltip": True,
"marketSearchDelay": 250,
"ammoChangeAll": False,

View File

@@ -263,6 +263,9 @@ bcs:
bcu:
- 'bcu'
- 'ballistic control system'
vts:
- 'vts'
- 'vorton tuning system'
tc:
- 'tc'
- '(?<!remote )tracking computer'

View File

@@ -17,7 +17,9 @@
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import pkg_resources
from importlib.resources import files
DEFAULT_DATA = pkg_resources.resource_string(__name__, 'defaults.yaml').decode()
DEFAULT_HEADER = pkg_resources.resource_string(__name__, 'header.yaml').decode()
PACKAGE_NAME = __name__.rsplit(".", maxsplit=1)[0]
DEFAULT_DATA = files(PACKAGE_NAME).joinpath('defaults.yaml').open('r', encoding='utf8').read()
DEFAULT_HEADER = files(PACKAGE_NAME).joinpath('header.yaml').open('r', encoding='utf8').read()

View File

@@ -184,7 +184,7 @@ class SearchWorkerThread(threading.Thread):
def _prepareRequestNormal(self, request):
# Escape regexp-specific symbols, and un-escape whitespaces
request = re.escape(request)
request = re.sub(r'\\(?P<ws>\s+)', '\g<ws>', request)
request = re.sub(r'\\(?P<ws>\s+)', r'\g<ws>', request)
# Imitate wildcard search
request = re.sub(r'\\\*', r'\\w*', request)
request = re.sub(r'\\\?', r'\\w?', request)
@@ -322,6 +322,11 @@ class Market:
"Geri" : self.les_grp, # AT18 prize
"Bestla" : self.les_grp, # AT18 prize
"Metamorphosis" : self.les_grp, # Seems to be anniversary gift
"Shapash" : self.les_grp, # AT19 prize
"Cybele" : self.les_grp, # AT19 prize
"Sidewinder" : self.les_grp, # AT20 prize
"Cobra" : self.les_grp, # AT20 prize
"Python" : self.les_grp, # AT20 prize
}
self.ITEMS_FORCEGROUP_R = self.__makeRevDict(self.ITEMS_FORCEGROUP)
@@ -983,3 +988,21 @@ class Market:
metatab = self.META_MAP_REVERSE_GROUPED.get(metagrpid)
metalvl = item.metaLevel or 0
return catname, mktgrpid, parentname, metatab, metalvl, item.name
def printAllItems(self):
items = set()
def handleMg(marketGroup, path=()):
marketGroup = self.getMarketGroup(marketGroup, eager=("items", "items.metaGroup", "children"))
path = path + (marketGroup.name,)
print(' > '.join(path))
for item in self.getItemsByMarketGroup(marketGroup):
items.add(item.ID)
for mgc in self.getMarketGroupChildren(marketGroup):
handleMg(mgc, path=path)
for mg in self.ROOT_MARKET_GROUPS:
handleMg(mg)
print(sorted(items))

View File

@@ -1 +1 @@
__all__ = ['evemarketer', 'evepraisal', 'evemarketdata', 'fuzzwork', 'cevemarket']
__all__ = ['evetycoon', 'evemarketdata', 'fuzzwork', 'cevemarket']

View File

@@ -1,75 +0,0 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
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
pyfalog = Logger(__name__)
class EveMarketer:
name = 'evemarketer'
group = 'tranquility'
def __init__(self, priceMap, system, fetchTimeout):
# Try selected system first
self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system)
# If price was not available - try globally
if priceMap:
self.fetchPrices(priceMap, max(fetchTimeout / 3, 2))
@staticmethod
def fetchPrices(priceMap, fetchTimeout, system=None):
params = {'typeid': {typeID for typeID in priceMap}}
if system is not None:
params['usesystem'] = system
baseurl = 'https://api.evemarketer.com/ec/marketstat'
network = Network.getInstance()
data = network.get(url=baseurl, type=network.PRICES, params=params, timeout=fetchTimeout)
xml = minidom.parseString(data.text)
types = xml.getElementsByTagName('marketstat').item(0).getElementsByTagName('type')
# Cycle through all types we've got from request
for type_ in types:
# Get data out of each typeID details tree
typeID = int(type_.getAttribute('id'))
sell = type_.getElementsByTagName('sell').item(0)
# If price data wasn't there, skip the item
try:
percprice = float(sell.getElementsByTagName('percentile').item(0).firstChild.data)
except (TypeError, ValueError):
pyfalog.warning('Failed to get price for: {0}', type_)
continue
# Price is 0 if evemarketer has info on this item, but it is not available
# for current scope limit. If we provided scope limit - make sure to skip
# such items to check globally, and do not skip if requested globally
if percprice == 0 and system is not None:
continue
priceMap[typeID].update(PriceStatus.fetchSuccess, percprice)
del priceMap[typeID]
Price.register(EveMarketer)

View File

@@ -17,7 +17,6 @@
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
from logbook import Logger
from eos.saveddata.price import PriceStatus
@@ -26,58 +25,41 @@ from service.price import Price
pyfalog = Logger(__name__)
systemAliases = {
None: 'universe',
30000142: 'jita',
30002187: 'amarr',
30002659: 'dodixie',
30002510: 'rens',
30002053: 'hek'}
locations = {
30000142: (10000002, 60003760), # Jita 4-4 CNAP
30002187: (10000043, 60008494), # Amarr VIII
30002659: (10000032, 60011866), # Dodixie
30002510: (10000030, 60004588), # Rens
30002053: (10000042, 60005686)} # Hek
class EvePraisal:
class EveTycoon:
name = 'evepraisal'
name = 'evetycoon'
group = 'tranquility'
def __init__(self, priceMap, system, fetchTimeout):
# Try selected system first
self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system)
# If price was not available - try globally
if priceMap:
self.fetchPrices(priceMap, max(fetchTimeout / 3, 2))
@staticmethod
def fetchPrices(priceMap, fetchTimeout, system=None):
if system not in systemAliases:
return
jsonData = {
'market_name': systemAliases[system],
'items': [{'type_id': typeID} for typeID in priceMap]}
baseurl = 'https://evepraisal.com/appraisal/structured.json'
# Default to jita when system is not found
regionID, stationID = locations.get(system, locations[30000142])
baseurl = 'https://evetycoon.com/api/v1/market/stats'
network = Network.getInstance()
resp = network.post(baseurl, network.PRICES, jsonData=jsonData, timeout=fetchTimeout)
data = resp.json()
try:
itemsData = data['appraisal']['items']
except (KeyError, TypeError):
return
# Cycle through all types we've got from request
for itemData in itemsData:
try:
typeID = int(itemData['typeID'])
price = itemData['prices']['sell']['min']
orderCount = itemData['prices']['sell']['order_count']
except (KeyError, TypeError):
for typeID in tuple(priceMap):
url = f'{baseurl}/{regionID}/{typeID}'
resp = network.get(url=url, params={'locationId': stationID}, type=network.PRICES, timeout=fetchTimeout)
if resp.status_code != 200:
continue
# evepraisal returns 0 if price data doesn't even exist for the item
price = resp.json()['sellAvgFivePercent']
# Price is 0 - no data
if price == 0:
continue
# evepraisal seems to provide price for some items despite having no orders up
if orderCount < 1:
continue
priceMap[typeID].update(PriceStatus.fetchSuccess, price)
del priceMap[typeID]
Price.register(EvePraisal)
Price.register(EveTycoon)

View File

@@ -1,2 +1,2 @@
from .efs import EfsPort
from .port import Port, IPortUser
from .port import Port

View File

@@ -423,7 +423,8 @@ class EfsPort:
else:
maxRange = stats.maxRange
dps_spread_dict = stats.getDps(spoolOptions=spoolOptions, getSpreadDPS=True)
dps = stats.getDps(spoolOptions=spoolOptions)
dps_spread_dict = {'em': dps.em, 'therm': dps.thermal, 'kin': dps.kinetic, 'exp': dps.explosive, 'pure': dps.pure}
dps_spread_dict.update((x, y*n) for x, y in dps_spread_dict.items())
statDict = {
@@ -637,7 +638,7 @@ class EfsPort:
bsGroupNames = ["Battleship", "Elite Battleship", "Black Ops", "Marauder"]
capitalGroupNames = ["Titan", "Dreadnought", "Freighter", "Carrier", "Supercarrier",
"Capital Industrial Ship", "Jump Freighter", "Force Auxiliary"]
indyGroupNames = ["Industrial", "Deep Space Transport", "Blockade Runner",
indyGroupNames = ["Hauler", "Deep Space Transport", "Blockade Runner",
"Mining Barge", "Exhumer", "Industrial Command Ship"]
miscGroupNames = ["Capsule", "Prototype Exploration Ship"]
shipSizes = [

View File

@@ -38,7 +38,7 @@ from service.const import PortEftOptions
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 service.port.shared import fetchItem
pyfalog = Logger(__name__)
@@ -46,7 +46,7 @@ pyfalog = Logger(__name__)
MODULE_CATS = ('Module', 'Subsystem', 'Structure Module')
SLOT_ORDER = (FittingSlot.LOW, FittingSlot.MED, FittingSlot.HIGH, FittingSlot.RIG, FittingSlot.SUBSYSTEM, FittingSlot.SERVICE)
OFFLINE_SUFFIX = '/OFFLINE'
NAME_CHARS = '[^,/\[\]]' # Characters which are allowed to be used in name
NAME_CHARS = r'[^,/\[\]]' # Characters which are allowed to be used in name
class MutationExportData:
@@ -176,7 +176,11 @@ def exportDrones(drones, exportMutants=True, mutaData=None, standAlone=True):
return drone.item.typeName
def droneSorter(drone):
groupName = Market.getInstance().getMarketGroupByItem(drone.item).marketGroupName
if drone.isMutated:
item = drone.baseItem
else:
item = drone.item
groupName = Market.getInstance().getMarketGroupByItem(item).marketGroupName
return (DRONE_ORDER.index(groupName), drone.isMutated, drone.fullName)
if mutaData is None:
@@ -200,7 +204,10 @@ def exportDrones(drones, exportMutants=True, mutaData=None, standAlone=True):
def exportFighters(fighters):
# Same as in drone additions panel
FIGHTER_ORDER = ('Light Fighter', 'Heavy Fighter', 'Support Fighter')
FIGHTER_ORDER = (
'Light Fighter', 'Structure Light Fighter',
'Heavy Fighter', 'Structure Heavy Fighter',
'Support Fighter', 'Structure Support Fighter')
def fighterSorter(fighter):
groupName = Market.getInstance().getGroupByItem(fighter.item).name
@@ -243,9 +250,9 @@ def importEft(lines):
aFit = AbstractFit()
aFit.mutations = importGetMutationData(lines)
stubPattern = '^\[.+?\]$'
modulePattern = '^(?P<typeName>{0}+?)(,\s*(?P<chargeName>{0}+?))?(?P<offline>\s*{1})?(\s*\[(?P<mutation>\d+?)\])?$'.format(NAME_CHARS, OFFLINE_SUFFIX)
droneCargoPattern = '^(?P<typeName>{}+?) x(?P<amount>\d+?)(\s*\[(?P<mutation>\d+?)\])?$'.format(NAME_CHARS)
stubPattern = r'^\[.+?\]$'
modulePattern = r'^(?P<typeName>{0}+?)(,\s*(?P<chargeName>{0}+?))?(?P<offline>\s*{1})?(\s*\[(?P<mutation>\d+?)\])?$'.format(NAME_CHARS, OFFLINE_SUFFIX)
droneCargoPattern = r'^(?P<typeName>{}+?) x(?P<amount>\d+?)(\s*\[(?P<mutation>\d+?)\])?$'.format(NAME_CHARS)
sections = []
for section in _importSectionIter(lines):
@@ -365,7 +372,7 @@ def importEft(lines):
return fit
def importEftCfg(shipname, lines, iportuser):
def importEftCfg(shipname, lines, progress):
"""Handle import from EFT config store file"""
# Check if we have such ship in database, bail if we don't
@@ -388,6 +395,8 @@ def importEftCfg(shipname, lines, iportuser):
fitIndices.append(startPos)
for i, startPos in enumerate(fitIndices):
if progress and progress.userCancelled:
return []
# End position is last file line if we're trying to get it for last fit,
# or start position of next fit minus 1
endPos = len(lines) if i == len(fitIndices) - 1 else fitIndices[i + 1]
@@ -413,17 +422,17 @@ def importEftCfg(shipname, lines, iportuser):
continue
# Parse line into some data we will need
misc = re.match("(Drones|Implant|Booster)_(Active|Inactive)=(.+)", line)
cargo = re.match("Cargohold=(.+)", line)
misc = re.match(r"(Drones|Implant|Booster)_(Active|Inactive)=(.+)", line)
cargo = re.match(r"Cargohold=(.+)", line)
# 2017/03/27 NOTE: store description from EFT
description = re.match("Description=(.+)", line)
description = re.match(r"Description=(.+)", line)
if misc:
entityType = misc.group(1)
entityState = misc.group(2)
entityData = misc.group(3)
if entityType == "Drones":
droneData = re.match("(.+),([0-9]+)", entityData)
droneData = re.match(r"(.+),([0-9]+)", entityData)
# Get drone name and attempt to detect drone number
droneName = droneData.group(1) if droneData else entityData
droneAmount = int(droneData.group(2)) if droneData else 1
@@ -489,7 +498,7 @@ def importEftCfg(shipname, lines, iportuser):
fitobj.boosters.append(b)
# If we don't have any prefixes, then it's a module
elif cargo:
cargoData = re.match("(.+),([0-9]+)", cargo.group(1))
cargoData = re.match(r"(.+),([0-9]+)", cargo.group(1))
cargoName = cargoData.group(1) if cargoData else cargo.group(1)
cargoAmount = int(cargoData.group(2)) if cargoData else 1
# Bail if we can't get item
@@ -508,7 +517,7 @@ def importEftCfg(shipname, lines, iportuser):
elif description:
fitobj.notes = description.group(1).replace("|", "\n")
else:
withCharge = re.match("(.+),(.+)", line)
withCharge = re.match(r"(.+),(.+)", line)
modName = withCharge.group(1) if withCharge else line
chargeName = withCharge.group(2) if withCharge else None
# If we can't get module item, skip it
@@ -558,11 +567,8 @@ def importEftCfg(shipname, lines, iportuser):
# Append fit to list of fits
fits.append(fitobj)
if iportuser: # NOTE: Send current processing status
processing_notify(
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
"%s:\n%s" % (fitobj.ship.name, fitobj.name)
)
if progress:
progress.message = "%s:\n%s" % (fitobj.ship.name, fitobj.name)
except (KeyboardInterrupt, SystemExit):
raise
@@ -585,7 +591,7 @@ def _importPrepare(lines):
return lines
mutantHeaderPattern = re.compile('^\[(?P<ref>\d+)\](?P<tail>.*)')
mutantHeaderPattern = re.compile(r'^\[(?P<ref>\d+)\](?P<tail>.*)')
def importGetMutationData(lines):
@@ -646,7 +652,7 @@ def _importCreateFit(lines):
"""Create fit and set top-level entity (ship or citadel)."""
fit = Fit()
header = lines.pop(0)
m = re.match('\[(?P<shipType>[^,]+),\s*(?P<fitName>.+)\]', header)
m = re.match(r'\[(?P<shipType>[^,]+),\s*(?P<fitName>.+)\]', header)
if not m:
pyfalog.warning('service.port.eft.importEft: corrupted fit header')
raise EftImportError
@@ -969,7 +975,7 @@ def lineIter(text):
def parseAdditions(text, mutaData=None):
items = []
sMkt = Market.getInstance()
pattern = '^(?P<typeName>{}+?)( x(?P<amount>\d+?))?(\s*\[(?P<mutaref>\d+?)\])?$'.format(NAME_CHARS)
pattern = r'^(?P<typeName>{}+?)( x(?P<amount>\d+?))?(\s*\[(?P<mutaref>\d+?)\])?$'.format(NAME_CHARS)
for line in lineIter(text):
m = re.match(pattern, line)
if not m:
@@ -992,7 +998,7 @@ def isValidDroneImport(text):
lines = list(lineIter(text))
mutaData = importGetMutationData(lines)
text = '\n'.join(lines)
pattern = 'x\d+(\s*\[\d+\])?$'
pattern = r'x\d+(\s*\[\d+\])?$'
for line in lineIter(text):
if not re.search(pattern, line):
return False, ()
@@ -1006,7 +1012,7 @@ def isValidDroneImport(text):
def isValidFighterImport(text):
pattern = 'x\d+$'
pattern = r'x\d+$'
for line in lineIter(text):
if not re.search(pattern, line):
return False, ()
@@ -1020,7 +1026,7 @@ def isValidFighterImport(text):
def isValidCargoImport(text):
pattern = 'x\d+$'
pattern = r'x\d+$'
for line in lineIter(text):
if not re.search(pattern, line):
return False, ()
@@ -1034,7 +1040,7 @@ def isValidCargoImport(text):
def isValidImplantImport(text):
pattern = 'x\d+$'
pattern = r'x\d+$'
for line in lineIter(text):
if re.search(pattern, line):
return False, ()
@@ -1048,7 +1054,7 @@ def isValidImplantImport(text):
def isValidBoosterImport(text):
pattern = 'x\d+$'
pattern = r'x\d+$'
for line in lineIter(text):
if re.search(pattern, line):
return False, ()

View File

@@ -56,7 +56,7 @@ INV_FLAG_DRONEBAY = 87
INV_FLAG_FIGHTER = 158
def exportESI(ofit, exportCharges, callback):
def exportESI(ofit, exportCharges, exportImplants, exportBoosters, callback):
# A few notes:
# max fit name length is 50 characters
# Most keys are created simply because they are required, but bogus data is okay
@@ -133,6 +133,22 @@ def exportESI(ofit, exportCharges, callback):
item['type_id'] = fighter.item.ID
fit['items'].append(item)
if exportImplants:
for implant in ofit.implants:
item = nested_dict()
item['flag'] = INV_FLAG_CARGOBAY
item['quantity'] = 1
item['type_id'] = implant.item.ID
fit['items'].append(item)
if exportBoosters:
for booster in ofit.boosters:
item = nested_dict()
item['flag'] = INV_FLAG_CARGOBAY
item['quantity'] = 1
item['type_id'] = booster.item.ID
fit['items'].append(item)
if len(fit['items']) == 0:
raise ESIExportException("Cannot export fitting: module list cannot be empty.")

View File

@@ -28,17 +28,20 @@ from service.esiAccess import EsiAccess
def renderMutant(mutant, firstPrefix='', prefix=''):
exportLines = []
exportLines.append('{}{}'.format(firstPrefix, mutant.baseItem.name))
exportLines.append('{}{}'.format(prefix, mutant.mutaplasmid.item.name))
exportLines.append('{}{}'.format(prefix, renderMutantAttrs(mutant)))
return '\n'.join(exportLines)
def renderMutantAttrs(mutant):
mutatedAttrs = {}
for attrID, mutator in mutant.mutators.items():
attrName = getAttributeInfo(attrID).name
mutatedAttrs[attrName] = mutator.value
exportLines.append('{}{}'.format(firstPrefix, mutant.baseItem.name))
exportLines.append('{}{}'.format(prefix, mutant.mutaplasmid.item.name))
customAttrsLine = ', '.join(
return ', '.join(
'{} {}'.format(a, floatUnerr(mutatedAttrs[a]))
for a in sorted(mutatedAttrs))
exportLines.append('{}{}'.format(prefix, customAttrsLine))
return '\n'.join(exportLines)
def parseMutant(lines):
@@ -64,8 +67,13 @@ def parseMutant(lines):
mutationsLine = lines[2]
except IndexError:
return baseItem, mutaplasmidItem, {}
mutations = parseMutantAttrs(mutationsLine)
return baseItem, mutaplasmidItem, mutations
def parseMutantAttrs(line):
mutations = {}
pairs = [p.strip() for p in mutationsLine.split(',')]
pairs = [p.strip() for p in line.split(',')]
for pair in pairs:
try:
attrName, value = pair.split(' ')
@@ -79,7 +87,7 @@ def parseMutant(lines):
if attrInfo is None:
continue
mutations[attrInfo.ID] = value
return baseItem, mutaplasmidItem, mutations
return mutations
def parseDynamicItemString(text):

View File

@@ -38,7 +38,6 @@ from service.port.eft import (
isValidImplantImport, isValidBoosterImport)
from service.port.esi import exportESI, importESI
from service.port.multibuy import exportMultiBuy
from service.port.shared import IPortUser, UserCancelException, processing_notify
from service.port.shipstats import exportFitStats
from service.port.xml import importXml, exportXml
from service.port.muta import parseMutant, parseDynamicItemString, fetchDynamicItem
@@ -73,53 +72,48 @@ class Port:
return cls.__tag_replace_flag
@staticmethod
def backupFits(path, iportuser):
def backupFits(path, progress):
pyfalog.debug("Starting backup fits thread.")
def backupFitsWorkerFunc(path, iportuser):
success = True
def backupFitsWorkerFunc(path, progress):
try:
iportuser.on_port_process_start()
backedUpFits = Port.exportXml(svcFit.getInstance().getAllFits(), iportuser)
backupFile = open(path, "w", encoding="utf-8")
backupFile.write(backedUpFits)
backupFile.close()
except UserCancelException:
success = False
# Send done signal to GUI
# wx.CallAfter(callback, -1, "Done.")
flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE
iportuser.on_port_processing(IPortUser.PROCESS_EXPORT | flag,
"User canceled or some error occurrence." if not success else "Done.")
backedUpFits = Port.exportXml(svcFit.getInstance().getAllFits(), progress)
if backedUpFits:
progress.message = f'writing {path}'
backupFile = open(path, "w", encoding="utf-8")
backupFile.write(backedUpFits)
backupFile.close()
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
progress.error = f'{e}'
finally:
progress.current += 1
progress.workerWorking = False
threading.Thread(
target=backupFitsWorkerFunc,
args=(path, iportuser)
args=(path, progress)
).start()
@staticmethod
def importFitsThreaded(paths, iportuser):
# type: (tuple, IPortUser) -> None
def importFitsThreaded(paths, progress):
"""
:param paths: fits data file path list.
:param iportuser: IPortUser implemented class.
:rtype: None
"""
pyfalog.debug("Starting import fits thread.")
def importFitsFromFileWorkerFunc(paths, iportuser):
iportuser.on_port_process_start()
success, result = Port.importFitFromFiles(paths, iportuser)
flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE
iportuser.on_port_processing(IPortUser.PROCESS_IMPORT | flag, result)
def importFitsFromFileWorkerFunc(paths, progress):
Port.importFitFromFiles(paths, progress)
threading.Thread(
target=importFitsFromFileWorkerFunc,
args=(paths, iportuser)
args=(paths, progress)
).start()
@staticmethod
def importFitFromFiles(paths, iportuser=None):
def importFitFromFiles(paths, progress=None):
"""
Imports fits from file(s). First processes all provided paths and stores
assembled fits into a list. This allows us to call back to the GUI as
@@ -132,11 +126,13 @@ class Port:
fit_list = []
try:
for path in paths:
if iportuser: # Pulse
if progress:
if progress and progress.userCancelled:
progress.workerWorking = False
return False, "Cancelled by user"
msg = "Processing file:\n%s" % path
progress.message = msg
pyfalog.debug(msg)
processing_notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg)
# wx.CallAfter(callback, 1, msg)
with open(path, "rb") as file_:
srcString = file_.read()
@@ -148,15 +144,21 @@ class Port:
continue
try:
importType, makesNewFits, fitsImport = Port.importAuto(srcString, path, iportuser=iportuser)
importType, makesNewFits, fitsImport = Port.importAuto(srcString, path, progress=progress)
fit_list += fitsImport
except xml.parsers.expat.ExpatError:
pyfalog.warning("Malformed XML in:\n{0}", path)
return False, "Malformed XML in %s" % path
msg = "Malformed XML in %s" % path
if progress:
progress.error = msg
progress.workerWorking = False
return False, msg
# IDs = [] # NOTE: what use for IDs?
numFits = len(fit_list)
for idx, fit in enumerate(fit_list):
if progress and progress.userCancelled:
progress.workerWorking = False
return False, "Cancelled by user"
# Set some more fit attributes and save
fit.character = sFit.character
fit.damagePattern = sFit.pattern
@@ -168,25 +170,23 @@ class Port:
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
db.save(fit)
# IDs.append(fit.ID)
if iportuser: # Pulse
if progress:
pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", idx + 1, numFits)
processing_notify(
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
"Processing complete, saving fits to database\n(%d/%d) %s" % (idx + 1, numFits, fit.ship.name)
)
except UserCancelException:
return False, "Processing has been canceled.\n"
progress.message = "Processing complete, saving fits to database\n(%d/%d) %s" % (idx + 1, numFits, fit.ship.name)
except (KeyboardInterrupt, SystemExit):
raise
except Exception as e:
pyfalog.critical("Unknown exception processing: {0}", path)
pyfalog.critical("Unknown exception processing: {0}", paths)
pyfalog.critical(e)
# TypeError: not all arguments converted during string formatting
# return False, "Unknown Error while processing {0}" % path
if progress:
progress.error = f'{e}'
progress.workerWorking = False
return False, "Unknown error while processing {}\n\n Error: {} {}".format(
path, type(e).__name__, getattr(e, 'message', ''))
paths, type(e).__name__, getattr(e, 'message', ''))
if progress:
progress.cbArgs.append(fit_list[:])
progress.workerWorking = False
return True, fit_list
@staticmethod
@@ -211,8 +211,7 @@ class Port:
return importType, importData
@classmethod
def importAuto(cls, string, path=None, activeFit=None, iportuser=None):
# type: (Port, str, str, object, IPortUser) -> object
def importAuto(cls, string, path=None, activeFit=None, progress=None):
lines = string.splitlines()
# Get first line and strip space symbols of it to avoid possible detection errors
firstLine = ''
@@ -224,7 +223,7 @@ class Port:
# If XML-style start of tag encountered, detect as XML
if re.search(RE_XML_START, firstLine):
return "XML", True, cls.importXml(string, iportuser)
return "XML", True, cls.importXml(string, progress)
# If JSON-style start, parse as CREST/JSON
if firstLine[0] == '{':
@@ -232,21 +231,21 @@ class Port:
# If we've got source file name which is used to describe ship name
# and first line contains something like [setup name], detect as eft config file
if re.match("^\s*\[.*\]", firstLine) and path is not None:
if re.match(r"^\s*\[.*\]", firstLine) and path is not None:
filename = os.path.split(path)[1]
shipName = filename.rsplit('.')[0]
return "EFT Config", True, cls.importEftCfg(shipName, lines, iportuser)
return "EFT Config", True, cls.importEftCfg(shipName, lines, progress)
# 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("^\s*\[.*,.*\]", firstLine):
if re.match(r"^\s*\[.*,.*\]", firstLine):
return "EFT", True, (cls.importEft(lines),)
# Check if string is in DNA format
dnaPattern = "\d+(:\d+(;\d+))*::"
dnaPattern = r"\d+(:\d+(;\d+))*::"
if re.match(dnaPattern, firstLine):
return "DNA", True, (cls.importDna(string),)
dnaChatPattern = "<url=fitting:(?P<dna>{})>(?P<fitName>[^<>]+)</url>".format(dnaPattern)
dnaChatPattern = r"<url=fitting:(?P<dna>{})>(?P<fitName>[^<>]+)</url>".format(dnaPattern)
m = re.search(dnaChatPattern, firstLine)
if m:
return "DNA", True, (cls.importDna(m.group("dna"), fitName=m.group("fitName")),)
@@ -297,8 +296,8 @@ class Port:
return importEft(lines)
@staticmethod
def importEftCfg(shipname, lines, iportuser=None):
return importEftCfg(shipname, lines, iportuser)
def importEftCfg(shipname, lines, progress=None):
return importEftCfg(shipname, lines, progress)
@classmethod
def exportEft(cls, fit, options, callback=None):
@@ -323,17 +322,17 @@ class Port:
return importESI(string)
@staticmethod
def exportESI(fit, exportCharges, callback=None):
return exportESI(fit, exportCharges, callback=callback)
def exportESI(fit, exportCharges, exportImplants, exportBoosters, callback=None):
return exportESI(fit, exportCharges, exportImplants, exportBoosters, callback=callback)
# XML-related methods
@staticmethod
def importXml(text, iportuser=None):
return importXml(text, iportuser)
def importXml(text, progress=None):
return importXml(text, progress)
@staticmethod
def exportXml(fits, iportuser=None, callback=None):
return exportXml(fits, iportuser, callback=callback)
def exportXml(fits, progress=None, callback=None):
return exportXml(fits, progress, callback=callback)
# Multibuy-related methods
@staticmethod

View File

@@ -18,8 +18,6 @@
# =============================================================================
from abc import ABCMeta, abstractmethod
from logbook import Logger
from service.market import Market
@@ -28,55 +26,6 @@ from service.market import Market
pyfalog = Logger(__name__)
class UserCancelException(Exception):
"""when user cancel on port processing."""
pass
class IPortUser(metaclass=ABCMeta):
ID_PULSE = 1
# Pulse the progress bar
ID_UPDATE = ID_PULSE << 1
# Replace message with data: update messate
ID_DONE = ID_PULSE << 2
# open fits: import process done
ID_ERROR = ID_PULSE << 3
# display error: raise some error
PROCESS_IMPORT = ID_PULSE << 4
# means import process.
PROCESS_EXPORT = ID_PULSE << 5
# means import process.
@abstractmethod
def on_port_processing(self, action, data=None):
"""
While importing fits from file, the logic calls back to this function to
update progress bar to show activity. XML files can contain multiple
ships with multiple fits, whereas EFT cfg files contain many fits of
a single ship. When iterating through the files, we update the message
when we start a new file, and then Pulse the progress bar with every fit
that is processed.
action : a flag that lets us know how to deal with :data
None: Pulse the progress bar
1: Replace message with data
other: Close dialog and handle based on :action (-1 open fits, -2 display error)
"""
"""return: True is continue process, False is cancel."""
pass
def on_port_process_start(self):
pass
def processing_notify(iportuser, flag, data):
if not iportuser.on_port_processing(flag, data):
raise UserCancelException
def fetchItem(typeName, eagerCat=False):
sMkt = Market.getInstance()
eager = 'group.category' if eagerCat else None

View File

@@ -24,6 +24,7 @@ import xml.parsers.expat
from logbook import Logger
from eos.const import FittingModuleState, FittingSlot
from eos.db import getDynamicItem
from eos.saveddata.cargo import Cargo
from eos.saveddata.citadel import Citadel
from eos.saveddata.drone import Drone
@@ -34,7 +35,8 @@ from eos.saveddata.ship import Ship
from gui.fitCommands.helpers import activeStateLimit
from service.fit import Fit as svcFit
from service.market import Market
from service.port.shared import IPortUser, processing_notify
from service.port.muta import renderMutantAttrs, parseMutantAttrs
from service.port.shared import fetchItem
from utils.strfunctions import replace_ltgt, sequential_rep
@@ -115,7 +117,7 @@ def _resolve_ship(fitting, sMkt, b_localized):
def _resolve_module(hardware, sMkt, b_localized):
# type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.module.Module
moduleName = hardware.getAttribute("type")
moduleName = hardware.getAttribute("base_type") or hardware.getAttribute("type")
emergency = None
if b_localized:
try:
@@ -142,12 +144,18 @@ def _resolve_module(hardware, sMkt, b_localized):
must_retry = True
if not must_retry:
break
return item
mutaplasmidName = hardware.getAttribute("mutaplasmid")
mutaplasmidItem = fetchItem(mutaplasmidName) if mutaplasmidName else None
mutatedAttrsText = hardware.getAttribute("mutated_attrs")
mutatedAttrs = parseMutantAttrs(mutatedAttrsText) if mutatedAttrsText else None
return item, mutaplasmidItem, mutatedAttrs
def importXml(text, iportuser):
def importXml(text, progress):
from .port import Port
# type: (str, IPortUser) -> list[eos.saveddata.fit.Fit]
sMkt = Market.getInstance()
doc = xml.dom.minidom.parseString(text)
# NOTE:
@@ -160,6 +168,9 @@ def importXml(text, iportuser):
failed = 0
for fitting in fittings:
if progress and progress.userCancelled:
return []
try:
fitobj = _resolve_ship(fitting, sMkt, b_localized)
except (KeyboardInterrupt, SystemExit):
@@ -185,12 +196,25 @@ def importXml(text, iportuser):
moduleList = []
for hardware in hardwares:
try:
item = _resolve_module(hardware, sMkt, b_localized)
item, mutaItem, mutaAttrs = _resolve_module(hardware, sMkt, b_localized)
if not item or not item.published:
continue
if item.category.name == "Drone":
d = Drone(item)
d = None
if mutaItem:
mutaplasmid = getDynamicItem(mutaItem.ID)
if mutaplasmid:
try:
d = Drone(mutaplasmid.resultingItem, item, mutaplasmid)
except ValueError:
pass
else:
for attrID, mutator in d.mutators.items():
if attrID in mutaAttrs:
mutator.value = mutaAttrs[attrID]
if d is None:
d = Drone(item)
d.amount = int(hardware.getAttribute("qty"))
fitobj.drones.append(d)
elif item.category.name == "Fighter":
@@ -205,8 +229,21 @@ def importXml(text, iportuser):
c.amount = int(hardware.getAttribute("qty"))
fitobj.cargo.append(c)
else:
m = None
try:
m = Module(item)
if mutaItem:
mutaplasmid = getDynamicItem(mutaItem.ID)
if mutaplasmid:
try:
m = Module(mutaplasmid.resultingItem, item, mutaplasmid)
except ValueError:
pass
else:
for attrID, mutator in m.mutators.items():
if attrID in mutaAttrs:
mutator.value = mutaAttrs[attrID]
if m is None:
m = Module(item)
# When item can't be added to any slot (unknown item or just charge), ignore it
except ValueError:
pyfalog.warning("item can't be added to any slot (unknown item or just charge), ignore it")
@@ -237,16 +274,13 @@ def importXml(text, iportuser):
fitobj.modules.append(module)
fit_list.append(fitobj)
if iportuser: # NOTE: Send current processing status
processing_notify(
iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE,
"Processing %s\n%s" % (fitobj.ship.name, fitobj.name)
)
if progress:
progress.message = "Processing %s\n%s" % (fitobj.ship.name, fitobj.name)
return fit_list
def exportXml(fits, iportuser, callback):
def exportXml(fits, progress, callback):
doc = xml.dom.minidom.Document()
fittings = doc.createElement("fittings")
# fit count
@@ -254,7 +288,18 @@ def exportXml(fits, iportuser, callback):
fittings.setAttribute("count", "%s" % fit_count)
doc.appendChild(fittings)
def addMutantAttributes(node, mutant):
node.setAttribute("base_type", mutant.baseItem.name)
node.setAttribute("mutaplasmid", mutant.mutaplasmid.item.name)
node.setAttribute("mutated_attrs", renderMutantAttrs(mutant))
for i, fit in enumerate(fits):
if progress:
if progress.userCancelled:
return None
processedFits = i + 1
progress.current = processedFits
progress.message = "converting to xml (%s/%s) %s" % (processedFits, fit_count, fit.ship.name)
try:
fitting = doc.createElement("fitting")
fitting.setAttribute("name", fit.name)
@@ -303,6 +348,9 @@ def exportXml(fits, iportuser, callback):
slotName = FittingSlot(slot).name.lower()
slotName = slotName if slotName != "high" else "hi"
hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId))
if module.isMutated:
addMutantAttributes(hardware, module)
fitting.appendChild(hardware)
if module.charge:
@@ -316,6 +364,9 @@ def exportXml(fits, iportuser, callback):
hardware.setAttribute("qty", "%d" % drone.amount)
hardware.setAttribute("slot", "drone bay")
hardware.setAttribute("type", drone.item.name)
if drone.isMutated:
addMutantAttributes(hardware, drone)
fitting.appendChild(hardware)
for fighter in fit.fighters:
@@ -341,12 +392,6 @@ def exportXml(fits, iportuser, callback):
except Exception as e:
pyfalog.error("Failed on fitID: %d, message: %s" % e.message)
continue
finally:
if iportuser:
processing_notify(
iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE,
(i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name))
)
text = doc.toprettyxml()
if callback:

View File

@@ -55,7 +55,7 @@ def version_precheck():
try:
import sqlalchemy
saMatch = re.match("([0-9]+).([0-9]+).([0-9]+)(([b\.])([0-9]+))?", sqlalchemy.__version__)
saMatch = re.match(r"([0-9]+).([0-9]+).([0-9]+)(([b\.])([0-9]+))?", sqlalchemy.__version__)
version_block += "\nSQLAlchemy version: {}".format(sqlalchemy.__version__)
if (int(saMatch.group(1)), int(saMatch.group(2)), int(saMatch.group(3))) < (1, 0, 5):

View File

@@ -276,4 +276,4 @@ class PriceWorkerThread(threading.Thread):
# Import market sources only to initialize price source modules, they register on their own
from service.marketSources import evemarketer, evemarketdata, evepraisal, fuzzwork, cevemarket # noqa: E402
from service.marketSources import evemarketdata, fuzzwork, cevemarket, evetycoon # noqa: E402

View File

@@ -222,9 +222,9 @@ class NetworkSettings:
if prefix not in proxydict:
continue
proxyline = proxydict[prefix]
proto = "{0}://".format(prefix)
if proxyline[:len(proto)] == proto:
proxyline = proxyline[len(proto):]
proto_pos = proxyline.find('://')
if proto_pos != -1:
proxyline = proxyline[proto_pos+3:]
# sometimes proxyline contains "user:password@" section before proxy address
# remove it if present, so later split by ":" works
if '@' in proxyline:
@@ -376,6 +376,8 @@ class EsiSettings:
"timeout": 60,
"server": "Tranquility",
"exportCharges": True,
"exportImplants": True,
"exportBoosters": True,
"enforceJwtExpiration": True
}
@@ -390,6 +392,9 @@ class EsiSettings:
def set(self, type, value):
self.settings[type] = value
def keys(self):
return config.supported_servers.keys()
class StatViewSettings:
_instance = None
@@ -558,7 +563,7 @@ class LocaleSettings:
with open(os.path.join(config.pyfaPath, 'locale', 'progress.json'), "r") as f:
self.progress_data = json.load(f)
except FileNotFoundError:
self.progress_data = None
self.progress_data = {}
@classmethod
def getInstance(cls):
@@ -567,14 +572,14 @@ class LocaleSettings:
return cls._instance
def get_progress(self, lang):
if self.progress_data is None:
if not self.progress_data:
return None
if lang == self.defaults['locale']:
return None
return self.progress_data[lang]
return self.progress_data.get(lang)
@classmethod
def supported_langauges(cls):
def supported_languages(cls):
"""Requires the application to be initialized, otherwise wx.Translation isn't set."""
pyfalog.info(f'using "{config.CATALOG}" to fetch languages, relatively base path "{os.getcwd()}"')
return {x: wx.Locale.FindLanguageInfo(x) for x in wx.Translations.Get().GetAvailableTranslations(config.CATALOG)}
@@ -589,6 +594,6 @@ class LocaleSettings:
return val if val != self.defaults['eos_locale'] else self.settings['locale'].split("_")[0]
def set(self, key, value):
if key == 'locale' and value not in self.supported_langauges():
if key == 'locale' and value not in self.supported_languages():
self.settings[key] = self.DEFAULT
self.settings[key] = value