diff --git a/eos/db/gamedata/queries.py b/eos/db/gamedata/queries.py index 90e757788..c6ee5c70b 100644 --- a/eos/db/gamedata/queries.py +++ b/eos/db/gamedata/queries.py @@ -396,6 +396,21 @@ def getAbyssalTypes(): return set([r.resultingTypeID for r in gamedata_session.query(DynamicItem.resultingTypeID).distinct()]) +@cachedQuery(1, "itemID") +def getDynamicItem(itemID, eager=None): + try: + if isinstance(itemID, int): + if eager is None: + result = gamedata_session.query(DynamicItem).filter(DynamicItem.ID == itemID).one() + else: + result = gamedata_session.query(DynamicItem).options(*processEager(eager)).filter(DynamicItem.ID == itemID).one() + else: + raise TypeError("Need integer as argument") + except exc.NoResultFound: + result = None + return result + + def getRequiredFor(itemID, attrMapping): Attribute1 = aliased(Attribute) Attribute2 = aliased(Attribute) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 194429d31..3b5206185 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -114,6 +114,7 @@ class HandledList(list): class HandledModuleList(HandledList): + def append(self, mod): emptyPosition = float("Inf") for i in range(len(self)): @@ -131,6 +132,9 @@ class HandledModuleList(HandledList): self.remove(mod) return + self.appendIgnoreEmpty(mod) + + def appendIgnoreEmpty(self, mod): mod.position = len(self) HandledList.append(self, mod) if mod.isInvalid: diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 9f5291026..10eff2647 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -20,51 +20,84 @@ # noinspection PyPackageRequirements import wx +from service.port.eft import EFT_OPTIONS +from service.settings import SettingsProvider class CopySelectDialog(wx.Dialog): copyFormatEft = 0 - copyFormatEftImps = 1 - copyFormatXml = 2 - copyFormatDna = 3 - copyFormatEsi = 4 - copyFormatMultiBuy = 5 - copyFormatEfs = 6 + copyFormatXml = 1 + copyFormatDna = 2 + copyFormatEsi = 3 + copyFormatMultiBuy = 4 + copyFormatEfs = 5 def __init__(self, parent): wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="Select a format", size=(-1, -1), style=wx.DEFAULT_DIALOG_STYLE) mainSizer = wx.BoxSizer(wx.VERTICAL) - copyFormats = ["EFT", "EFT (Implants)", "XML", "DNA", "ESI", "MultiBuy", "EFS"] - copyFormatTooltips = {CopySelectDialog.copyFormatEft: "EFT text format", - CopySelectDialog.copyFormatEftImps: "EFT text format", - CopySelectDialog.copyFormatXml: "EVE native XML format", - CopySelectDialog.copyFormatDna: "A one-line text format", - CopySelectDialog.copyFormatEsi: "A JSON format used for ESI", - CopySelectDialog.copyFormatMultiBuy: "MultiBuy text format", - CopySelectDialog.copyFormatEfs: "JSON data format used by EFS"} - selector = wx.RadioBox(self, wx.ID_ANY, label="Copy to the clipboard using:", choices=copyFormats, - style=wx.RA_SPECIFY_ROWS) - selector.Bind(wx.EVT_RADIOBOX, self.Selected) - for format, tooltip in copyFormatTooltips.items(): - selector.SetItemToolTip(format, tooltip) + self.settings = SettingsProvider.getInstance().getSettings("pyfaExport", {"format": 0, "options": 0}) - self.copyFormat = CopySelectDialog.copyFormatEft - selector.SetSelection(self.copyFormat) + self.copyFormats = { + "EFT": CopySelectDialog.copyFormatEft, + "XML": CopySelectDialog.copyFormatXml, + "DNA": CopySelectDialog.copyFormatDna, + "ESI": CopySelectDialog.copyFormatEsi, + "MultiBuy": CopySelectDialog.copyFormatMultiBuy, + "EFS": CopySelectDialog.copyFormatEfs + } - mainSizer.Add(selector, 0, wx.EXPAND | wx.ALL, 5) + 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) + else: + rdo = wx.RadioButton(self, wx.ID_ANY, format) + rdo.Bind(wx.EVT_RADIOBUTTON, self.Selected) + if self.settings['format'] == self.copyFormats[format]: + rdo.SetValue(True) + self.copyFormat = self.copyFormats[format] + mainSizer.Add(rdo, 0, wx.EXPAND | wx.ALL, 5) + + if format == "EFT": + bsizer = wx.BoxSizer(wx.VERTICAL) + + 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) + mainSizer.Add(bsizer, 1, wx.EXPAND | wx.LEFT, 20) buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL) if buttonSizer: mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, 5) + self.toggleOptions() self.SetSizer(mainSizer) self.Fit() self.Center() def Selected(self, event): - self.copyFormat = event.GetSelection() + obj = event.GetEventObject() + format = obj.GetLabel() + self.copyFormat = self.copyFormats[format] + self.toggleOptions() + self.Fit() + + def toggleOptions(self): + for ch in self.options.values(): + ch.Enable(self.GetSelected() == CopySelectDialog.copyFormatEft) 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 diff --git a/gui/esiFittings.py b/gui/esiFittings.py index af3da77a5..c471a1334 100644 --- a/gui/esiFittings.py +++ b/gui/esiFittings.py @@ -15,7 +15,7 @@ import gui.globalEvents as GE from logbook import Logger from service.esi import Esi from service.esiAccess import APIException -from service.port import ESIExportException +from service.port.esi import ESIExportException pyfalog = Logger(__name__) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index a4befdeda..b8277e8cb 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -76,8 +76,7 @@ from service.esiAccess import SsoMode from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues from eos.db.saveddata.queries import getFit as db_getFit -from service.port import Port, IPortUser -from service.efsPort import EfsPort +from service.port import Port, IPortUser, EfsPort from service.settings import HTMLExportSettings from time import gmtime, strftime @@ -711,31 +710,31 @@ class MainFrame(wx.Frame): else: self.marketBrowser.search.Focus() - def clipboardEft(self): + def clipboardEft(self, options): fit = db_getFit(self.getActiveFit()) - toClipboard(Port.exportEft(fit)) + toClipboard(Port.exportEft(fit, options)) - def clipboardEftImps(self): + def clipboardEftImps(self, options): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportEftImps(fit)) - def clipboardDna(self): + def clipboardDna(self, options): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportDna(fit)) - def clipboardEsi(self): + def clipboardEsi(self, options): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportESI(fit)) - def clipboardXml(self): + def clipboardXml(self, options): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportXml(None, fit)) - def clipboardMultiBuy(self): + def clipboardMultiBuy(self, options): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportMultiBuy(fit)) - def clipboardEfs(self): + def clipboardEfs(self, options): fit = db_getFit(self.getActiveFit()) toClipboard(EfsPort.exportEfs(fit, 0)) @@ -750,7 +749,7 @@ class MainFrame(wx.Frame): def exportToClipboard(self, event): CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft, - CopySelectDialog.copyFormatEftImps: self.clipboardEftImps, + # CopySelectDialog.copyFormatEftImps: self.clipboardEftImps, CopySelectDialog.copyFormatXml: self.clipboardXml, CopySelectDialog.copyFormatDna: self.clipboardDna, CopySelectDialog.copyFormatEsi: self.clipboardEsi, @@ -759,8 +758,13 @@ class MainFrame(wx.Frame): dlg = CopySelectDialog(self) dlg.ShowModal() selected = dlg.GetSelected() + options = dlg.GetOptions() - CopySelectDict[selected]() + settings = SettingsProvider.getInstance().getSettings("pyfaExport") + settings["format"] = selected + settings["options"] = options + + CopySelectDict[selected](options) try: dlg.Destroy() diff --git a/service/port.py b/service/port.py deleted file mode 100644 index 5ad5c4ed3..000000000 --- a/service/port.py +++ /dev/null @@ -1,1399 +0,0 @@ -# ============================================================================= -# Copyright (C) 2014 Ryan Holmes -# -# 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 . -# ============================================================================= - -import re -import os -import xml.dom -from logbook import Logger -import collections -import json -import threading -from bs4 import UnicodeDammit - - -from codecs import open - -import xml.parsers.expat - -from eos import db -from eos.db.gamedata.queries import getAttributeInfo -from service.fit import Fit as svcFit - -# noinspection PyPackageRequirements -import wx - -from eos.saveddata.cargo import Cargo -from eos.saveddata.implant import Implant -from eos.saveddata.booster import Booster -from eos.saveddata.drone import Drone -from eos.saveddata.fighter import Fighter -from eos.saveddata.module import Module, State, Slot -from eos.saveddata.ship import Ship -from eos.saveddata.citadel import Citadel -from eos.saveddata.fit import Fit, ImplantLocation -from service.market import Market -from utils.strfunctions import sequential_rep, replace_ltgt -from abc import ABCMeta, abstractmethod - -from service.esi import Esi -from collections import OrderedDict - - -class ESIExportException(Exception): - pass - - -pyfalog = Logger(__name__) - -EFT_SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE] -INV_FLAGS = { - Slot.LOW: 11, - Slot.MED: 19, - Slot.HIGH: 27, - Slot.RIG: 92, - Slot.SUBSYSTEM: 125, - Slot.SERVICE: 164 -} - -INV_FLAG_CARGOBAY = 5 -INV_FLAG_DRONEBAY = 87 -INV_FLAG_FIGHTER = 158 - -# 2017/04/05 NOTE: simple validation, for xml file -RE_XML_START = r'<\?xml\s+version="1.0"\s*\?>' - -# -- 170327 Ignored description -- -RE_LTGT = "&(lt|gt);" -L_MARK = "<localized hint="" -# <localized hint="([^"]+)">([^\*]+)\*<\/localized> -LOCALIZED_PATTERN = re.compile(r'([^\*]+)\*') - - -def _extract_match(t): - m = LOCALIZED_PATTERN.match(t) - # hint attribute, text content - return m.group(1), m.group(2) - - -def _resolve_ship(fitting, sMkt, b_localized): - # type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.fit.Fit - """ NOTE: Since it is meaningless unless a correct ship object can be constructed, - process flow changed - """ - # ------ Confirm ship - # Maelstrom - shipType = fitting.getElementsByTagName("shipType").item(0).getAttribute("value") - anything = None - if b_localized: - # expect an official name, emergency cache - shipType, anything = _extract_match(shipType) - - limit = 2 - ship = None - while True: - must_retry = False - try: - try: - ship = Ship(sMkt.getItem(shipType)) - except ValueError: - ship = Citadel(sMkt.getItem(shipType)) - except Exception as e: - pyfalog.warning("Caught exception on _resolve_ship") - pyfalog.error(e) - limit -= 1 - if limit is 0: - break - shipType = anything - must_retry = True - if not must_retry: - break - - if ship is None: - raise Exception("cannot resolve ship type.") - - fitobj = Fit(ship=ship) - # ------ Confirm fit name - anything = fitting.getAttribute("name") - # 2017/03/29 NOTE: - # if fit name contained "<" or ">" then reprace to named html entity by EVE client - # if re.search(RE_LTGT, anything): - if "<" in anything or ">" in anything: - anything = replace_ltgt(anything) - fitobj.name = anything - - return fitobj - - -def _resolve_module(hardware, sMkt, b_localized): - # type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.module.Module - moduleName = hardware.getAttribute("type") - emergency = None - if b_localized: - # expect an official name, emergency cache - moduleName, emergency = _extract_match(moduleName) - - item = None - limit = 2 - while True: - must_retry = False - try: - item = sMkt.getItem(moduleName, eager="group.category") - except Exception as e: - pyfalog.warning("Caught exception on _resolve_module") - pyfalog.error(e) - limit -= 1 - if limit is 0: - break - moduleName = emergency - must_retry = True - if not must_retry: - break - return item - - -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 - - -class Port(object): - """ - 2017/03/31 NOTE: About change - 1. want to keep the description recorded in fit - 2. i think should not write wx.CallAfter in here - """ - instance = None - __tag_replace_flag = True - - @classmethod - def getInstance(cls): - if cls.instance is None: - cls.instance = Port() - - return cls.instance - - @classmethod - def set_tag_replace(cls, b): - cls.__tag_replace_flag = b - - @classmethod - def is_tag_replace(cls): - # might there is a person who wants to hold tags. - # (item link in EVE client etc. When importing again to EVE) - return cls.__tag_replace_flag - - @staticmethod - def backupFits(path, iportuser): - pyfalog.debug("Starting backup fits thread.") -# thread = FitBackupThread(path, callback) -# thread.start() - threading.Thread( - target=PortProcessing.backupFits, - args=(path, iportuser) - ).start() - - @staticmethod - def importFitsThreaded(paths, iportuser): - # type: (tuple, IPortUser) -> None - """ - :param paths: fits data file path list. - :param iportuser: IPortUser implemented class. - :rtype: None - """ - pyfalog.debug("Starting import fits thread.") -# thread = FitImportThread(paths, iportuser) -# thread.start() - threading.Thread( - target=PortProcessing.importFitsFromFile, - args=(paths, iportuser) - ).start() - - @staticmethod - def importFitFromFiles(paths, iportuser=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 - fits are processed as well as when fits are being saved. - returns - """ - - sFit = svcFit.getInstance() - - fit_list = [] - try: - for path in paths: - if iportuser: # Pulse - msg = "Processing file:\n%s" % path - pyfalog.debug(msg) - PortProcessing.notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg) - # wx.CallAfter(callback, 1, msg) - - with open(path, "rb") as file_: - srcString = file_.read() - dammit = UnicodeDammit(srcString) - srcString = dammit.unicode_markup - - if len(srcString) == 0: # ignore blank files - pyfalog.debug("File is blank.") - continue - - try: - _, fitsImport = Port.importAuto(srcString, path, iportuser=iportuser) - fit_list += fitsImport - except xml.parsers.expat.ExpatError: - pyfalog.warning("Malformed XML in:\n{0}", path) - return False, "Malformed XML in %s" % path - - # IDs = [] # NOTE: what use for IDs? - numFits = len(fit_list) - for idx, fit in enumerate(fit_list): - # Set some more fit attributes and save - fit.character = sFit.character - fit.damagePattern = sFit.pattern - fit.targetResists = sFit.targetResists - if len(fit.implants) > 0: - fit.implantLocation = ImplantLocation.FIT - else: - useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"] - fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT - db.save(fit) - # IDs.append(fit.ID) - if iportuser: # Pulse - pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", idx + 1, numFits) - PortProcessing.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" - except Exception as e: - pyfalog.critical("Unknown exception processing: {0}", path) - pyfalog.critical(e) - # TypeError: not all arguments converted during string formatting -# return False, "Unknown Error while processing {0}" % path - return False, "Unknown error while processing %s\n\n Error: %s" % (path, e.message) - - return True, fit_list - - @staticmethod - def importFitFromBuffer(bufferStr, activeFit=None): - # type: (basestring, object) -> object - # TODO: catch the exception? - # activeFit is reserved?, bufferStr is unicode? (assume only clipboard string? - sFit = svcFit.getInstance() - _, fits = Port.importAuto(bufferStr, activeFit=activeFit) - for fit in fits: - fit.character = sFit.character - fit.damagePattern = sFit.pattern - fit.targetResists = sFit.targetResists - if len(fit.implants) > 0: - fit.implantLocation = ImplantLocation.FIT - else: - useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"] - fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT - db.save(fit) - return fits - - """Service which houses all import/export format functions""" - - @classmethod - def exportESI(cls, ofit, callback=None): - # A few notes: - # max fit name length is 50 characters - # Most keys are created simply because they are required, but bogus data is okay - - nested_dict = lambda: collections.defaultdict(nested_dict) - fit = nested_dict() - sFit = svcFit.getInstance() - - # max length is 50 characters - name = ofit.name[:47] + '...' if len(ofit.name) > 50 else ofit.name - fit['name'] = name - fit['ship_type_id'] = ofit.ship.item.ID - - # 2017/03/29 NOTE: "<" or "<" is Ignored - # fit['description'] = "" % ofit.ID - fit['description'] = ofit.notes[:397] + '...' if len(ofit.notes) > 400 else ofit.notes if ofit.notes is not None else "" - fit['items'] = [] - - slotNum = {} - charges = {} - for module in ofit.modules: - if module.isEmpty: - continue - - item = nested_dict() - slot = module.slot - - if slot == Slot.SUBSYSTEM: - # Order of subsystem matters based on this attr. See GH issue #130 - slot = int(module.getModifiedItemAttr("subSystemSlot")) - item['flag'] = slot - else: - if slot not in slotNum: - slotNum[slot] = INV_FLAGS[slot] - - item['flag'] = slotNum[slot] - slotNum[slot] += 1 - - item['quantity'] = 1 - item['type_id'] = module.item.ID - fit['items'].append(item) - - if module.charge and sFit.serviceFittingOptions["exportCharges"]: - if module.chargeID not in charges: - charges[module.chargeID] = 0 - # `or 1` because some charges (ie scripts) are without qty - charges[module.chargeID] += module.numCharges or 1 - - for cargo in ofit.cargo: - item = nested_dict() - item['flag'] = INV_FLAG_CARGOBAY - item['quantity'] = cargo.amount - item['type_id'] = cargo.item.ID - fit['items'].append(item) - - for chargeID, amount in list(charges.items()): - item = nested_dict() - item['flag'] = INV_FLAG_CARGOBAY - item['quantity'] = amount - item['type_id'] = chargeID - fit['items'].append(item) - - for drone in ofit.drones: - item = nested_dict() - item['flag'] = INV_FLAG_DRONEBAY - item['quantity'] = drone.amount - item['type_id'] = drone.item.ID - fit['items'].append(item) - - for fighter in ofit.fighters: - item = nested_dict() - item['flag'] = INV_FLAG_FIGHTER - item['quantity'] = fighter.amountActive - item['type_id'] = fighter.item.ID - fit['items'].append(item) - - if len(fit['items']) == 0: - raise ESIExportException("Cannot export fitting: module list cannot be empty.") - - return json.dumps(fit) - - @classmethod - def importAuto(cls, string, path=None, activeFit=None, iportuser=None): - # type: (basestring, basestring, object, IPortUser, basestring) -> object - # 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() - - # If XML-style start of tag encountered, detect as XML - if re.search(RE_XML_START, firstLine): - return "XML", cls.importXml(string, iportuser) - - # If JSON-style start, parse as CREST/JSON - if firstLine[0] == '{': - return "JSON", (cls.importESI(string),) - - # 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("\[.*\]", firstLine) and path is not None: - filename = os.path.split(path)[1] - shipName = filename.rsplit('.')[0] - return "EFT Config", cls.importEftCfg(shipName, string, 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),) - - # Use DNA format for all other cases - return "DNA", (cls.importDna(string),) - - @staticmethod - def importESI(str_): - - sMkt = Market.getInstance() - fitobj = Fit() - refobj = json.loads(str_) - items = refobj['items'] - # "<" and ">" is replace to "<", ">" by EVE client - fitobj.name = refobj['name'] - # 2017/03/29: read description - fitobj.notes = refobj['description'] - - try: - ship = refobj['ship_type_id'] - try: - fitobj.ship = Ship(sMkt.getItem(ship)) - except ValueError: - fitobj.ship = Citadel(sMkt.getItem(ship)) - except: - pyfalog.warning("Caught exception in importESI") - return None - - items.sort(key=lambda k: k['flag']) - - moduleList = [] - for module in items: - try: - item = sMkt.getItem(module['type_id'], eager="group.category") - if not item.published: - continue - if module['flag'] == INV_FLAG_DRONEBAY: - d = Drone(item) - d.amount = module['quantity'] - fitobj.drones.append(d) - elif module['flag'] == INV_FLAG_CARGOBAY: - c = Cargo(item) - c.amount = module['quantity'] - fitobj.cargo.append(c) - elif module['flag'] == INV_FLAG_FIGHTER: - fighter = Fighter(item) - fitobj.fighters.append(fighter) - else: - try: - m = Module(item) - # When item can't be added to any slot (unknown item or just charge), ignore it - except ValueError: - pyfalog.debug("Item can't be added to any slot (unknown item or just charge)") - continue - # Add subsystems before modules to make sure T3 cruisers have subsystems installed - if item.category.name == "Subsystem": - if m.fits(fitobj): - fitobj.modules.append(m) - else: - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE - - moduleList.append(m) - - except: - pyfalog.warning("Could not process module.") - continue - - # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(fitobj) - - for module in moduleList: - if module.fits(fitobj): - fitobj.modules.append(module) - - return fitobj - - @staticmethod - def importDna(string): - sMkt = Market.getInstance() - - ids = list(map(int, re.findall(r'\d+', string))) - for id_ in ids: - try: - try: - try: - Ship(sMkt.getItem(sMkt.getItem(id_))) - except ValueError: - Citadel(sMkt.getItem(sMkt.getItem(id_))) - except ValueError: - Citadel(sMkt.getItem(id_)) - string = string[string.index(str(id_)):] - break - except: - pyfalog.warning("Exception caught in importDna") - pass - string = string[:string.index("::") + 2] - info = string.split(":") - - f = Fit() - try: - try: - f.ship = Ship(sMkt.getItem(int(info[0]))) - except ValueError: - f.ship = Citadel(sMkt.getItem(int(info[0]))) - f.name = "{0} - DNA Imported".format(f.ship.item.name) - except UnicodeEncodeError: - def logtransform(s_): - if len(s_) > 10: - return s_[:10] + "..." - return s_ - - pyfalog.exception("Couldn't import ship data {0}", [logtransform(s) for s in info]) - return None - - moduleList = [] - for itemInfo in info[1:]: - if itemInfo: - itemID, amount = itemInfo.split(";") - item = sMkt.getItem(int(itemID), eager="group.category") - - if item.category.name == "Drone": - d = Drone(item) - d.amount = int(amount) - f.drones.append(d) - elif item.category.name == "Fighter": - ft = Fighter(item) - ft.amount = int(amount) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize - if ft.fits(f): - f.fighters.append(ft) - elif item.category.name == "Charge": - c = Cargo(item) - c.amount = int(amount) - f.cargo.append(c) - else: - for i in range(int(amount)): - try: - m = Module(item) - except: - pyfalog.warning("Exception caught in importDna") - continue - # Add subsystems before modules to make sure T3 cruisers have subsystems installed - if item.category.name == "Subsystem": - if m.fits(f): - f.modules.append(m) - else: - m.owner = f - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE - moduleList.append(m) - - # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(f) - - for module in moduleList: - if module.fits(f): - module.owner = f - if module.isValidState(State.ACTIVE): - module.state = State.ACTIVE - f.modules.append(module) - - return f - - @staticmethod - def importEft(eftString): - sMkt = Market.getInstance() - offineSuffix = " /OFFLINE" - - fit = Fit() - eftString = eftString.strip() - lines = re.split('[\n\r]+', eftString) - info = lines[0][1:-1].split(",", 1) - - if len(info) == 2: - shipType = info[0].strip() - fitName = info[1].strip() - else: - shipType = info[0].strip() - fitName = "Imported %s" % shipType - - try: - ship = sMkt.getItem(shipType) - try: - fit.ship = Ship(ship) - except ValueError: - fit.ship = Citadel(ship) - fit.name = fitName - except: - pyfalog.warning("Exception caught in importEft") - return - - # maintain map of drones and their quantities - droneMap = {} - cargoMap = {} - moduleList = [] - for i in range(1, len(lines)): - ammoName = None - extraAmount = None - - line = lines[i].strip() - if not line: - continue - - setOffline = line.endswith(offineSuffix) - if setOffline is True: - # remove offline suffix from line - line = line[:len(line) - len(offineSuffix)] - - modAmmo = line.split(",") - # matches drone and cargo with x{qty} - modExtra = modAmmo[0].split(" x") - - if len(modAmmo) == 2: - # line with a module and ammo - ammoName = modAmmo[1].strip() - modName = modAmmo[0].strip() - elif len(modExtra) == 2: - # line with drone/cargo and qty - extraAmount = modExtra[1].strip() - modName = modExtra[0].strip() - else: - # line with just module - modName = modExtra[0].strip() - - try: - # get item information. If we are on a Drone/Cargo line, throw out cargo - item = sMkt.getItem(modName, eager="group.category") - except: - # if no data can be found (old names) - pyfalog.warning("no data can be found (old names)") - continue - - if not item.published: - continue - - if item.category.name == "Drone": - extraAmount = int(extraAmount) if extraAmount is not None else 1 - if modName not in droneMap: - droneMap[modName] = 0 - droneMap[modName] += extraAmount - elif item.category.name == "Fighter": - extraAmount = int(extraAmount) if extraAmount is not None else 1 - fighterItem = Fighter(item) - if extraAmount > fighterItem.fighterSquadronMaxSize: # Amount bigger then max fightergroup size - extraAmount = fighterItem.fighterSquadronMaxSize - if fighterItem.fits(fit): - fit.fighters.append(fighterItem) - - if len(modExtra) == 2 and item.category.name != "Drone" and item.category.name != "Fighter": - extraAmount = int(extraAmount) if extraAmount is not None else 1 - if modName not in cargoMap: - cargoMap[modName] = 0 - cargoMap[modName] += extraAmount - elif item.category.name == "Implant": - if "implantness" in item.attributes: - fit.implants.append(Implant(item)) - elif "boosterness" in item.attributes: - fit.boosters.append(Booster(item)) - else: - pyfalog.error("Failed to import implant: {0}", line) - # elif item.category.name == "Subsystem": - # try: - # subsystem = Module(item) - # except ValueError: - # continue - # - # if subsystem.fits(fit): - # fit.modules.append(subsystem) - else: - try: - m = Module(item) - except ValueError: - continue - # Add subsystems before modules to make sure T3 cruisers have subsystems installed - if item.category.name == "Subsystem": - if m.fits(fit): - fit.modules.append(m) - else: - if ammoName: - try: - ammo = sMkt.getItem(ammoName) - if m.isValidCharge(ammo) and m.charge is None: - m.charge = ammo - except: - pass - - if setOffline is True and m.isValidState(State.OFFLINE): - m.state = State.OFFLINE - elif m.isValidState(State.ACTIVE): - m.state = State.ACTIVE - - moduleList.append(m) - - # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(fit) - - for m in moduleList: - if m.fits(fit): - m.owner = fit - if not m.isValidState(m.state): - pyfalog.warning("Error: Module {0} cannot have state {1}", m, m.state) - - fit.modules.append(m) - - for droneName in droneMap: - d = Drone(sMkt.getItem(droneName)) - d.amount = droneMap[droneName] - fit.drones.append(d) - - for cargoName in cargoMap: - c = Cargo(sMkt.getItem(cargoName)) - c.amount = cargoMap[cargoName] - fit.cargo.append(c) - - return fit - - @staticmethod - def importEftCfg(shipname, contents, iportuser=None): - """Handle import from EFT config store file""" - - # Check if we have such ship in database, bail if we don't - sMkt = Market.getInstance() - try: - sMkt.getItem(shipname) - except: - return [] # empty list is expected - - 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 - if line[:1] == "[" and line[-1:] == "]": - # Line index where current fit starts - startPos = lines.index(line) - fitIndices.append(startPos) - - for i, startPos in enumerate(fitIndices): - # 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] - - # Finally, get lines for current fitting - fitLines = lines[startPos:endPos] - - try: - # Create fit object - fitobj = Fit() - # Strip square brackets and pull out a fit name - fitobj.name = fitLines[0][1:-1] - # Assign ship to fitting - try: - fitobj.ship = Ship(sMkt.getItem(shipname)) - except ValueError: - fitobj.ship = Citadel(sMkt.getItem(shipname)) - - moduleList = [] - for x in range(1, len(fitLines)): - line = fitLines[x] - if not line: - continue - - # Parse line into some data we will need - misc = re.match("(Drones|Implant|Booster)_(Active|Inactive)=(.+)", line) - cargo = re.match("Cargohold=(.+)", line) - # 2017/03/27 NOTE: store description from EFT - description = re.match("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) - # 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 - # Bail if we can't get item or it's not from drone category - try: - droneItem = sMkt.getItem(droneName, eager="group.category") - except: - pyfalog.warning("Cannot get item.") - continue - if droneItem.category.name == "Drone": - # Add drone to the fitting - d = Drone(droneItem) - d.amount = droneAmount - if entityState == "Active": - d.amountActive = droneAmount - elif entityState == "Inactive": - d.amountActive = 0 - fitobj.drones.append(d) - elif droneItem.category.name == "Fighter": # EFT saves fighter as drones - ft = Fighter(droneItem) - ft.amount = int(droneAmount) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize - fitobj.fighters.append(ft) - else: - continue - elif entityType == "Implant": - # Bail if we can't get item or it's not from implant category - try: - implantItem = sMkt.getItem(entityData, eager="group.category") - except: - pyfalog.warning("Cannot get item.") - continue - if implantItem.category.name != "Implant": - continue - # Add implant to the fitting - imp = Implant(implantItem) - if entityState == "Active": - imp.active = True - elif entityState == "Inactive": - imp.active = False - fitobj.implants.append(imp) - elif entityType == "Booster": - # Bail if we can't get item or it's not from implant category - try: - boosterItem = sMkt.getItem(entityData, eager="group.category") - except: - pyfalog.warning("Cannot get item.") - continue - # All boosters have implant category - if boosterItem.category.name != "Implant": - continue - # Add booster to the fitting - b = Booster(boosterItem) - if entityState == "Active": - b.active = True - elif entityState == "Inactive": - b.active = False - 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)) - 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 - try: - item = sMkt.getItem(cargoName) - except: - pyfalog.warning("Cannot get item.") - continue - # Add Cargo to the fitting - c = Cargo(item) - c.amount = cargoAmount - fitobj.cargo.append(c) - # 2017/03/27 NOTE: store description from EFT - elif description: - fitobj.notes = description.group(1).replace("|", "\n") - else: - withCharge = re.match("(.+),(.+)", 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 - try: - modItem = sMkt.getItem(modName) - except: - pyfalog.warning("Cannot get item.") - continue - - # Create module - m = Module(modItem) - - # Add subsystems before modules to make sure T3 cruisers have subsystems installed - if modItem.category.name == "Subsystem": - if m.fits(fitobj): - fitobj.modules.append(m) - else: - m.owner = fitobj - # Activate mod if it is activable - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE - # Add charge to mod if applicable, on any errors just don't add anything - if chargeName: - try: - chargeItem = sMkt.getItem(chargeName, eager="group.category") - if chargeItem.category.name == "Charge": - m.charge = chargeItem - except: - pyfalog.warning("Cannot get item.") - pass - # Append module to fit - moduleList.append(m) - - # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(fitobj) - - for module in moduleList: - if module.fits(fitobj): - fitobj.modules.append(module) - - # Append fit to list of fits - fits.append(fitobj) - - if iportuser: # NOTE: Send current processing status - PortProcessing.notify( - iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, - "%s:\n%s" % (fitobj.ship.name, fitobj.name) - ) - - # Skip fit silently if we get an exception - except Exception as e: - pyfalog.error("Caught exception on fit.") - pyfalog.error(e) - pass - - return fits - - @staticmethod - def importXml(text, iportuser=None): - # type: (basestring, IPortUser, basestring) -> list[eos.saveddata.fit.Fit] - sMkt = Market.getInstance() - doc = xml.dom.minidom.parseString(text) - # NOTE: - # When L_MARK is included at this point, - # Decided to be localized data - b_localized = L_MARK in text - fittings = doc.getElementsByTagName("fittings").item(0) - fittings = fittings.getElementsByTagName("fitting") - fit_list = [] - failed = 0 - - for fitting in fittings: - try: - fitobj = _resolve_ship(fitting, sMkt, b_localized) - except: - failed += 1 - continue - - # -- 170327 Ignored description -- - # read description from exported xml. (EVE client, EFT) - description = fitting.getElementsByTagName("description").item(0).getAttribute("value") - if description is None: - description = "" - elif len(description): - # convert
to "\n" and remove html tags. - if Port.is_tag_replace(): - description = replace_ltgt( - sequential_rep(description, r"<(br|BR)>", "\n", r"<[^<>]+>", "") - ) - fitobj.notes = description - - hardwares = fitting.getElementsByTagName("hardware") - moduleList = [] - for hardware in hardwares: - try: - item = _resolve_module(hardware, sMkt, b_localized) - if not item or not item.published: - continue - - if item.category.name == "Drone": - d = Drone(item) - d.amount = int(hardware.getAttribute("qty")) - fitobj.drones.append(d) - elif item.category.name == "Fighter": - ft = Fighter(item) - ft.amount = int(hardware.getAttribute("qty")) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize - fitobj.fighters.append(ft) - elif hardware.getAttribute("slot").lower() == "cargo": - # although the eve client only support charges in cargo, third-party programs - # may support items or "refits" in cargo. Support these by blindly adding all - # cargo, not just charges - c = Cargo(item) - c.amount = int(hardware.getAttribute("qty")) - fitobj.cargo.append(c) - else: - try: - 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") - continue - # Add subsystems before modules to make sure T3 cruisers have subsystems installed - if item.category.name == "Subsystem": - if m.fits(fitobj): - m.owner = fitobj - fitobj.modules.append(m) - else: - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE - - moduleList.append(m) - - except KeyboardInterrupt: - pyfalog.warning("Keyboard Interrupt") - continue - - # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(fitobj) - - for module in moduleList: - if module.fits(fitobj): - module.owner = fitobj - fitobj.modules.append(module) - - fit_list.append(fitobj) - if iportuser: # NOTE: Send current processing status - PortProcessing.notify( - iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, - "Processing %s\n%s" % (fitobj.ship.name, fitobj.name) - ) - - return fit_list - - - @classmethod - def exportEft(cls, fit, mutations=False, implants=False): - # EFT formatted export is split in several sections, each section is - # separated from another using 2 blank lines. Sections might have several - # sub-sections, which are separated by 1 blank line - sections = [] - - header = '[{}, {}]'.format(fit.ship.item.name, fit.name) - - # Section 1: modules, rigs, subsystems, services - def formatAttrVal(val): - if int(val) == val: - return int(val) - return val - - offineSuffix = ' /OFFLINE' - modsBySlotType = {} - sFit = svcFit.getInstance() - for module in fit.modules: - slot = module.slot - slotTypeMods = modsBySlotType.setdefault(slot, []) - if module.item: - mutatedMod = bool(module.mutators) - # if module was mutated, use base item name for export - if mutatedMod: - modName = module.baseItem.name - else: - modName = module.item.name - modOfflineSuffix = offineSuffix if module.state == State.OFFLINE else '' - if module.charge and sFit.serviceFittingOptions['exportCharges']: - slotTypeMods.append('{}, {}{}'.format(modName, module.charge.name, modOfflineSuffix)) - else: - slotTypeMods.append('{}{}'.format(modName, modOfflineSuffix)) - if mutatedMod and mutations: - mutationGrade = module.mutaplasmid.item.name.split(' ', 1)[0].lower() - mutatedAttrs = {} - for attrID, mutator in module.mutators.items(): - attrName = getAttributeInfo(attrID).name - mutatedAttrs[attrName] = mutator.value - customAttrsLine = ', '.join('{} {}'.format(a, formatAttrVal(mutatedAttrs[a])) for a in sorted(mutatedAttrs)) - slotTypeMods.append(' {}: {}'.format(mutationGrade, customAttrsLine)) - else: - slotTypeMods.append('[Empty {} slot]'.format(Slot.getName(slot).capitalize() if slot is not None else '')) - modSection = [] - for slotType in EFT_SLOT_ORDER: - rackLines = [] - data = modsBySlotType.get(slotType, ()) - for line in data: - rackLines.append(line) - if rackLines: - modSection.append('\n'.join(rackLines)) - if modSection: - sections.append('\n\n'.join(modSection)) - - # Section 2: drones, fighters - minionSection = [] - droneLines = [] - for drone in sorted(fit.drones, key=lambda d: d.item.name): - droneLines.append('{} x{}'.format(drone.item.name, drone.amount)) - if droneLines: - minionSection.append('\n'.join(droneLines)) - fighterLines = [] - for fighter in sorted(fit.fighters, key=lambda f: f.item.name): - fighterLines.append('{} x{}'.format(fighter.item.name, fighter.amountActive)) - if fighterLines: - minionSection.append('\n'.join(fighterLines)) - if minionSection: - sections.append('\n\n'.join(minionSection)) - - # Section 3: implants, boosters - if implants: - charSection = [] - implantLines = [] - for implant in fit.implants: - implantLines.append(implant.item.name) - if implantLines: - charSection.append('\n'.join(implantLines)) - boosterLines = [] - for booster in fit.boosters: - boosterLines.append(booster.item.name) - if boosterLines: - charSection.append('\n'.join(boosterLines)) - if charSection: - sections.append('\n\n'.join(charSection)) - - # Section 4: cargo - cargoLines = [] - for cargo in sorted(fit.cargo, key=lambda c: (c.item.group.category.name, c.item.group.name, c.item.name)): - cargoLines.append('{} x{}'.format(cargo.item.name, cargo.amount)) - if cargoLines: - sections.append('\n'.join(cargoLines)) - - return '{}\n\n{}'.format(header, '\n\n\n'.join(sections)) - - @classmethod - def exportEftImps(cls, fit): - return cls.exportEft(fit, implants=True) - - @staticmethod - def exportDna(fit): - dna = str(fit.shipID) - subsystems = [] # EVE cares which order you put these in - mods = OrderedDict() - charges = OrderedDict() - sFit = svcFit.getInstance() - for mod in fit.modules: - if not mod.isEmpty: - if mod.slot == Slot.SUBSYSTEM: - subsystems.append(mod) - continue - if mod.itemID not in mods: - mods[mod.itemID] = 0 - mods[mod.itemID] += 1 - - if mod.charge and sFit.serviceFittingOptions["exportCharges"]: - if mod.chargeID not in charges: - charges[mod.chargeID] = 0 - # `or 1` because some charges (ie scripts) are without qty - charges[mod.chargeID] += mod.numCharges or 1 - - for subsystem in sorted(subsystems, key=lambda mod_: mod_.getModifiedItemAttr("subSystemSlot")): - dna += ":{0};1".format(subsystem.itemID) - - for mod in mods: - dna += ":{0};{1}".format(mod, mods[mod]) - - for drone in fit.drones: - dna += ":{0};{1}".format(drone.itemID, drone.amount) - - for fighter in fit.fighters: - dna += ":{0};{1}".format(fighter.itemID, fighter.amountActive) - - for fighter in fit.fighters: - dna += ":{0};{1}".format(fighter.itemID, fighter.amountActive) - - for cargo in fit.cargo: - # DNA format is a simple/dumb format. As CCP uses the slot information of the item itself - # without designating slots in the DNA standard, we need to make sure we only include - # charges in the DNA export. If modules were included, the EVE Client will interpret these - # as being "Fitted" to whatever slot they are for, and it causes an corruption error in the - # client when trying to save the fit - if cargo.item.category.name == "Charge": - if cargo.item.ID not in charges: - charges[cargo.item.ID] = 0 - charges[cargo.item.ID] += cargo.amount - - for charge in charges: - dna += ":{0};{1}".format(charge, charges[charge]) - - return dna + "::" - - @staticmethod - def exportXml(iportuser=None, *fits): - doc = xml.dom.minidom.Document() - fittings = doc.createElement("fittings") - # fit count - fit_count = len(fits) - fittings.setAttribute("count", "%s" % fit_count) - doc.appendChild(fittings) - sFit = svcFit.getInstance() - - for i, fit in enumerate(fits): - try: - fitting = doc.createElement("fitting") - fitting.setAttribute("name", fit.name) - fittings.appendChild(fitting) - description = doc.createElement("description") - # -- 170327 Ignored description -- - try: - notes = fit.notes # unicode - - if notes: - notes = notes[:397] + '...' if len(notes) > 400 else notes - - description.setAttribute( - "value", re.sub("(\r|\n|\r\n)+", "
", notes) if notes is not None else "" - ) - except Exception as e: - pyfalog.warning("read description is failed, msg=%s\n" % e.args) - - fitting.appendChild(description) - shipType = doc.createElement("shipType") - shipType.setAttribute("value", fit.ship.name) - fitting.appendChild(shipType) - - charges = {} - slotNum = {} - for module in fit.modules: - if module.isEmpty: - continue - - slot = module.slot - - if slot == Slot.SUBSYSTEM: - # Order of subsystem matters based on this attr. See GH issue #130 - slotId = module.getModifiedItemAttr("subSystemSlot") - 125 - else: - if slot not in slotNum: - slotNum[slot] = 0 - - slotId = slotNum[slot] - slotNum[slot] += 1 - - hardware = doc.createElement("hardware") - hardware.setAttribute("type", module.item.name) - slotName = Slot.getName(slot).lower() - slotName = slotName if slotName != "high" else "hi" - hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId)) - fitting.appendChild(hardware) - - if module.charge and sFit.serviceFittingOptions["exportCharges"]: - if module.charge.name not in charges: - charges[module.charge.name] = 0 - # `or 1` because some charges (ie scripts) are without qty - charges[module.charge.name] += module.numCharges or 1 - - for drone in fit.drones: - hardware = doc.createElement("hardware") - hardware.setAttribute("qty", "%d" % drone.amount) - hardware.setAttribute("slot", "drone bay") - hardware.setAttribute("type", drone.item.name) - fitting.appendChild(hardware) - - for fighter in fit.fighters: - hardware = doc.createElement("hardware") - hardware.setAttribute("qty", "%d" % fighter.amountActive) - hardware.setAttribute("slot", "fighter bay") - hardware.setAttribute("type", fighter.item.name) - fitting.appendChild(hardware) - - for cargo in fit.cargo: - if cargo.item.name not in charges: - charges[cargo.item.name] = 0 - charges[cargo.item.name] += cargo.amount - - for name, qty in list(charges.items()): - hardware = doc.createElement("hardware") - hardware.setAttribute("qty", "%d" % qty) - hardware.setAttribute("slot", "cargo") - hardware.setAttribute("type", name) - fitting.appendChild(hardware) - except Exception as e: - # print("Failed on fitID: %d" % fit.ID) - pyfalog.error("Failed on fitID: %d, message: %s" % e.message) - continue - finally: - if iportuser: - PortProcessing.notify( - iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE, - (i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name)) - ) -# wx.CallAfter(callback, i, "(%s/%s) %s" % (i, fit_count, fit.ship.name)) - - return doc.toprettyxml() - - @staticmethod - def exportMultiBuy(fit): - export = "%s\n" % fit.ship.item.name - stuff = {} - sFit = svcFit.getInstance() - for module in fit.modules: - slot = module.slot - if slot not in stuff: - stuff[slot] = [] - curr = "%s\n" % module.item.name if module.item else "" - if module.charge and sFit.serviceFittingOptions["exportCharges"]: - curr += "%s x%s\n" % (module.charge.name, module.numCharges) - stuff[slot].append(curr) - - for slotType in EFT_SLOT_ORDER: - data = stuff.get(slotType) - if data is not None: - # export += "\n" - for curr in data: - export += curr - - if len(fit.drones) > 0: - for drone in fit.drones: - export += "%s x%s\n" % (drone.item.name, drone.amount) - - if len(fit.cargo) > 0: - for cargo in fit.cargo: - export += "%s x%s\n" % (cargo.item.name, cargo.amount) - - if len(fit.implants) > 0: - for implant in fit.implants: - export += "%s\n" % implant.item.name - - if len(fit.boosters) > 0: - for booster in fit.boosters: - export += "%s\n" % booster.item.name - - if len(fit.fighters) > 0: - for fighter in fit.fighters: - export += "%s x%s\n" % (fighter.item.name, fighter.amountActive) - - if export[-1] == "\n": - export = export[:-1] - - return export - - -class PortProcessing(object): - """Port Processing class """ - @staticmethod - def backupFits(path, iportuser): - success = True - try: - iportuser.on_port_process_start() - backedUpFits = Port.exportXml(iportuser, *svcFit.getInstance().getAllFits()) - 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.") - - @staticmethod - def importFitsFromFile(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) - - @staticmethod - def notify(iportuser, flag, data): - if not iportuser.on_port_processing(flag, data): - raise UserCancelException diff --git a/service/port/__init__.py b/service/port/__init__.py new file mode 100644 index 000000000..2e884d214 --- /dev/null +++ b/service/port/__init__.py @@ -0,0 +1,2 @@ +from .efs import EfsPort +from .port import Port, IPortUser diff --git a/service/port/dna.py b/service/port/dna.py new file mode 100644 index 000000000..bd2645ef8 --- /dev/null +++ b/service/port/dna.py @@ -0,0 +1,176 @@ +# ============================================================================= +# Copyright (C) 2014 Ryan Holmes +# +# 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 . +# ============================================================================= + + +import re +from collections import OrderedDict + +from logbook import Logger + +from eos.saveddata.cargo import Cargo +from eos.saveddata.citadel import Citadel +from eos.saveddata.drone import Drone +from eos.saveddata.fighter import Fighter +from eos.saveddata.fit import Fit +from eos.saveddata.module import Module, State, Slot +from eos.saveddata.ship import Ship +from service.fit import Fit as svcFit +from service.market import Market + + +pyfalog = Logger(__name__) + + +def importDna(string): + sMkt = Market.getInstance() + + ids = list(map(int, re.findall(r'\d+', string))) + for id_ in ids: + try: + try: + try: + Ship(sMkt.getItem(sMkt.getItem(id_))) + except ValueError: + Citadel(sMkt.getItem(sMkt.getItem(id_))) + except ValueError: + Citadel(sMkt.getItem(id_)) + string = string[string.index(str(id_)):] + break + except: + pyfalog.warning("Exception caught in importDna") + pass + string = string[:string.index("::") + 2] + info = string.split(":") + + f = Fit() + try: + try: + f.ship = Ship(sMkt.getItem(int(info[0]))) + except ValueError: + f.ship = Citadel(sMkt.getItem(int(info[0]))) + f.name = "{0} - DNA Imported".format(f.ship.item.name) + except UnicodeEncodeError: + def logtransform(s_): + if len(s_) > 10: + return s_[:10] + "..." + return s_ + + pyfalog.exception("Couldn't import ship data {0}", [logtransform(s) for s in info]) + return None + + moduleList = [] + for itemInfo in info[1:]: + if itemInfo: + itemID, amount = itemInfo.split(";") + item = sMkt.getItem(int(itemID), eager="group.category") + + if item.category.name == "Drone": + d = Drone(item) + d.amount = int(amount) + f.drones.append(d) + elif item.category.name == "Fighter": + ft = Fighter(item) + ft.amount = int(amount) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize + if ft.fits(f): + f.fighters.append(ft) + elif item.category.name == "Charge": + c = Cargo(item) + c.amount = int(amount) + f.cargo.append(c) + else: + for i in range(int(amount)): + try: + m = Module(item) + except: + pyfalog.warning("Exception caught in importDna") + continue + # Add subsystems before modules to make sure T3 cruisers have subsystems installed + if item.category.name == "Subsystem": + if m.fits(f): + f.modules.append(m) + else: + m.owner = f + if m.isValidState(State.ACTIVE): + m.state = State.ACTIVE + moduleList.append(m) + + # Recalc to get slot numbers correct for T3 cruisers + svcFit.getInstance().recalc(f) + + for module in moduleList: + if module.fits(f): + module.owner = f + if module.isValidState(State.ACTIVE): + module.state = State.ACTIVE + f.modules.append(module) + + return f + + +def exportDna(fit): + dna = str(fit.shipID) + subsystems = [] # EVE cares which order you put these in + mods = OrderedDict() + charges = OrderedDict() + sFit = svcFit.getInstance() + for mod in fit.modules: + if not mod.isEmpty: + if mod.slot == Slot.SUBSYSTEM: + subsystems.append(mod) + continue + if mod.itemID not in mods: + mods[mod.itemID] = 0 + mods[mod.itemID] += 1 + + if mod.charge and sFit.serviceFittingOptions["exportCharges"]: + if mod.chargeID not in charges: + charges[mod.chargeID] = 0 + # `or 1` because some charges (ie scripts) are without qty + charges[mod.chargeID] += mod.numCharges or 1 + + for subsystem in sorted(subsystems, key=lambda mod_: mod_.getModifiedItemAttr("subSystemSlot")): + dna += ":{0};1".format(subsystem.itemID) + + for mod in mods: + dna += ":{0};{1}".format(mod, mods[mod]) + + for drone in fit.drones: + dna += ":{0};{1}".format(drone.itemID, drone.amount) + + for fighter in fit.fighters: + dna += ":{0};{1}".format(fighter.itemID, fighter.amountActive) + + for fighter in fit.fighters: + dna += ":{0};{1}".format(fighter.itemID, fighter.amountActive) + + for cargo in fit.cargo: + # DNA format is a simple/dumb format. As CCP uses the slot information of the item itself + # without designating slots in the DNA standard, we need to make sure we only include + # charges in the DNA export. If modules were included, the EVE Client will interpret these + # as being "Fitted" to whatever slot they are for, and it causes an corruption error in the + # client when trying to save the fit + if cargo.item.category.name == "Charge": + if cargo.item.ID not in charges: + charges[cargo.item.ID] = 0 + charges[cargo.item.ID] += cargo.amount + + for charge in charges: + dna += ":{0};{1}".format(charge, charges[charge]) + + return dna + "::" diff --git a/service/efsPort.py b/service/port/efs.py similarity index 100% rename from service/efsPort.py rename to service/port/efs.py diff --git a/service/port/eft.py b/service/port/eft.py new file mode 100644 index 000000000..fc8576d01 --- /dev/null +++ b/service/port/eft.py @@ -0,0 +1,878 @@ +# ============================================================================= +# Copyright (C) 2014 Ryan Holmes +# +# 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 . +# ============================================================================= + + +import re + +from logbook import Logger + +from eos.db.gamedata.queries import getAttributeInfo, getDynamicItem +from eos.saveddata.cargo import Cargo +from eos.saveddata.citadel import Citadel +from eos.saveddata.booster import Booster +from eos.saveddata.drone import Drone +from eos.saveddata.fighter import Fighter +from eos.saveddata.implant import Implant +from eos.saveddata.module import Module, State, Slot +from eos.saveddata.ship import Ship +from eos.saveddata.fit import Fit +from gui.utils.numberFormatter import roundToPrec +from service.fit import Fit as svcFit +from service.market import Market +from service.port.shared import IPortUser, processing_notify +from enum import Enum + + +pyfalog = Logger(__name__) + + +class Options(Enum): + IMPLANTS = 1 + MUTATIONS = 2 + + +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 + # separated from another using 2 blank lines. Sections might have several + # sub-sections, which are separated by 1 blank line + sections = [] + + header = '[{}, {}]'.format(fit.ship.item.name, fit.name) + + # Section 1: modules, rigs, subsystems, services + modsBySlotType = {} + sFit = svcFit.getInstance() + for module in fit.modules: + modsBySlotType.setdefault(module.slot, []).append(module) + modSection = [] + + mutants = {} # Format: {reference number: module} + mutantReference = 1 + for slotType in SLOT_ORDER: + rackLines = [] + 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: + modName = module.baseItem.name + else: + modName = module.item.name + if mutated 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']: + rackLines.append('{}, {}{}{}'.format( + modName, module.charge.name, modOfflineSuffix, mutationSuffix)) + else: + rackLines.append('{}{}{}'.format(modName, modOfflineSuffix, mutationSuffix)) + else: + rackLines.append('[Empty {} slot]'.format( + Slot.getName(slotType).capitalize() if slotType is not None else '')) + if rackLines: + modSection.append('\n'.join(rackLines)) + if modSection: + sections.append('\n\n'.join(modSection)) + + # Section 2: drones, fighters + minionSection = [] + droneLines = [] + for drone in sorted(fit.drones, key=lambda d: d.item.name): + droneLines.append('{} x{}'.format(drone.item.name, drone.amount)) + if droneLines: + minionSection.append('\n'.join(droneLines)) + fighterLines = [] + for fighter in sorted(fit.fighters, key=lambda f: f.item.name): + fighterLines.append('{} x{}'.format(fighter.item.name, fighter.amountActive)) + if fighterLines: + minionSection.append('\n'.join(fighterLines)) + if minionSection: + sections.append('\n\n'.join(minionSection)) + + # Section 3: implants, boosters + if options & Options.IMPLANTS.value: + charSection = [] + implantLines = [] + for implant in fit.implants: + implantLines.append(implant.item.name) + if implantLines: + charSection.append('\n'.join(implantLines)) + boosterLines = [] + for booster in fit.boosters: + boosterLines.append(booster.item.name) + if boosterLines: + charSection.append('\n'.join(boosterLines)) + if charSection: + sections.append('\n\n'.join(charSection)) + + # Section 4: cargo + cargoLines = [] + for cargo in sorted( + fit.cargo, + key=lambda c: (c.item.group.category.name, c.item.group.name, c.item.name) + ): + cargoLines.append('{} x{}'.format(cargo.item.name, cargo.amount)) + if cargoLines: + sections.append('\n'.join(cargoLines)) + + # Section 5: mutated modules' details + mutationLines = [] + if mutants and options & Options.MUTATIONS.value: + for mutantReference in sorted(mutants): + mutant = mutants[mutantReference] + mutatedAttrs = {} + for attrID, mutator in mutant.mutators.items(): + attrName = getAttributeInfo(attrID).name + mutatedAttrs[attrName] = mutator.value + mutationLines.append('[{}] {}'.format(mutantReference, mutant.baseItem.name)) + mutationLines.append(' {}'.format(mutant.mutaplasmid.item.name)) + # Round to 7th significant number to avoid exporting float errors + customAttrsLine = ', '.join( + '{} {}'.format(a, roundToPrec(mutatedAttrs[a], 7)) + for a in sorted(mutatedAttrs)) + mutationLines.append(' {}'.format(customAttrsLine)) + if mutationLines: + sections.append('\n'.join(mutationLines)) + + return '{}\n\n{}'.format(header, '\n\n\n'.join(sections)) + + +def importEft(eftString): + lines = _importPrepareString(eftString) + try: + fit = _importCreateFit(lines) + except EftImportError: + return + + aFit = AbstractFit() + aFit.mutations = _importGetMutationData(lines) + + nameChars = '[^,/\[\]]' # Characters which are allowed to be used in name + stubPattern = '^\[.+?\]$' + modulePattern = '^(?P{0}+?)(,\s*(?P{0}+?))?(?P\s*{1})?(\s*\[(?P\d+?)\])?$'.format(nameChars, OFFLINE_SUFFIX) + droneCargoPattern = '^(?P{}+?) x(?P\d+?)$'.format(nameChars) + + sections = [] + for section in _importSectionIter(lines): + for line in section.lines: + # Stub line + if re.match(stubPattern, line): + section.itemSpecs.append(None) + continue + # Items with quantity specifier + m = re.match(droneCargoPattern, line) + if m: + try: + itemSpec = MultiItemSpec(m.group('typeName')) + # Items which cannot be fetched are considered as stubs + except EftImportError: + section.itemSpecs.append(None) + else: + itemSpec.amount = int(m.group('amount')) + section.itemSpecs.append(itemSpec) + continue + # All other items + m = re.match(modulePattern, line) + if m: + try: + itemSpec = RegularItemSpec(m.group('typeName'), chargeName=m.group('chargeName')) + # Items which cannot be fetched are considered as stubs + except EftImportError: + section.itemSpecs.append(None) + else: + if m.group('offline'): + itemSpec.offline = True + if m.group('mutation'): + itemSpec.mutationIdx = int(m.group('mutation')) + section.itemSpecs.append(itemSpec) + continue + _clearTail(section.itemSpecs) + sections.append(section) + + hasDroneBay = any(s.isDroneBay for s in sections) + hasFighterBay = any(s.isFighterBay for s in sections) + for section in sections: + if section.isModuleRack: + aFit.addModules(section.itemSpecs) + elif section.isImplantRack: + for itemSpec in section.itemSpecs: + aFit.addImplant(itemSpec) + elif section.isDroneBay: + for itemSpec in section.itemSpecs: + aFit.addDrone(itemSpec) + elif section.isFighterBay: + for itemSpec in section.itemSpecs: + aFit.addFighter(itemSpec) + elif section.isCargoHold: + for itemSpec in section.itemSpecs: + aFit.addCargo(itemSpec) + # Mix between different kinds of item specs (can happen when some + # blank lines are removed) + else: + for itemSpec in section.itemSpecs: + if itemSpec is None: + continue + if itemSpec.isModule: + aFit.addModule(itemSpec) + elif itemSpec.isImplant: + aFit.addImplant(itemSpec) + elif itemSpec.isDrone and not hasDroneBay: + aFit.addDrone(itemSpec) + elif itemSpec.isFighter and not hasFighterBay: + aFit.addFighter(itemSpec) + elif itemSpec.isCargo: + aFit.addCargo(itemSpec) + + # Subsystems first because they modify slot amount + for m in aFit.subsystems: + if m is None: + dummy = Module.buildEmpty(aFit.getSlotByContainer(aFit.subsystems)) + dummy.owner = fit + fit.modules.appendIgnoreEmpty(dummy) + elif m.fits(fit): + m.owner = fit + fit.modules.appendIgnoreEmpty(m) + svcFit.getInstance().recalc(fit) + + # Other stuff + for modRack in ( + aFit.rigs, + aFit.services, + aFit.modulesHigh, + aFit.modulesMed, + aFit.modulesLow, + ): + for m in modRack: + if m is None: + dummy = Module.buildEmpty(aFit.getSlotByContainer(modRack)) + dummy.owner = fit + fit.modules.appendIgnoreEmpty(dummy) + elif m.fits(fit): + m.owner = fit + if not m.isValidState(m.state): + pyfalog.warning('service.port.eft.importEft: module {} cannot have state {}', m, m.state) + fit.modules.appendIgnoreEmpty(m) + for implant in aFit.implants: + fit.implants.append(implant) + for booster in aFit.boosters: + fit.boosters.append(booster) + for drone in aFit.drones.values(): + fit.drones.append(drone) + for fighter in aFit.fighters: + fit.fighters.append(fighter) + for cargo in aFit.cargo.values(): + fit.cargo.append(cargo) + + return fit + + +def importEftCfg(shipname, contents, iportuser): + """Handle import from EFT config store file""" + + # Check if we have such ship in database, bail if we don't + sMkt = Market.getInstance() + try: + sMkt.getItem(shipname) + except: + return [] # empty list is expected + + 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 + if line[:1] == "[" and line[-1:] == "]": + # Line index where current fit starts + startPos = lines.index(line) + fitIndices.append(startPos) + + for i, startPos in enumerate(fitIndices): + # 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] + + # Finally, get lines for current fitting + fitLines = lines[startPos:endPos] + + try: + # Create fit object + fitobj = Fit() + # Strip square brackets and pull out a fit name + fitobj.name = fitLines[0][1:-1] + # Assign ship to fitting + try: + fitobj.ship = Ship(sMkt.getItem(shipname)) + except ValueError: + fitobj.ship = Citadel(sMkt.getItem(shipname)) + + moduleList = [] + for x in range(1, len(fitLines)): + line = fitLines[x] + if not line: + continue + + # Parse line into some data we will need + misc = re.match("(Drones|Implant|Booster)_(Active|Inactive)=(.+)", line) + cargo = re.match("Cargohold=(.+)", line) + # 2017/03/27 NOTE: store description from EFT + description = re.match("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) + # 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 + # Bail if we can't get item or it's not from drone category + try: + droneItem = sMkt.getItem(droneName, eager="group.category") + except: + pyfalog.warning("Cannot get item.") + continue + if droneItem.category.name == "Drone": + # Add drone to the fitting + d = Drone(droneItem) + d.amount = droneAmount + if entityState == "Active": + d.amountActive = droneAmount + elif entityState == "Inactive": + d.amountActive = 0 + fitobj.drones.append(d) + elif droneItem.category.name == "Fighter": # EFT saves fighter as drones + ft = Fighter(droneItem) + ft.amount = int(droneAmount) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize + fitobj.fighters.append(ft) + else: + continue + elif entityType == "Implant": + # Bail if we can't get item or it's not from implant category + try: + implantItem = sMkt.getItem(entityData, eager="group.category") + except: + pyfalog.warning("Cannot get item.") + continue + if implantItem.category.name != "Implant": + continue + # Add implant to the fitting + imp = Implant(implantItem) + if entityState == "Active": + imp.active = True + elif entityState == "Inactive": + imp.active = False + fitobj.implants.append(imp) + elif entityType == "Booster": + # Bail if we can't get item or it's not from implant category + try: + boosterItem = sMkt.getItem(entityData, eager="group.category") + except: + pyfalog.warning("Cannot get item.") + continue + # All boosters have implant category + if boosterItem.category.name != "Implant": + continue + # Add booster to the fitting + b = Booster(boosterItem) + if entityState == "Active": + b.active = True + elif entityState == "Inactive": + b.active = False + 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)) + 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 + try: + item = sMkt.getItem(cargoName) + except: + pyfalog.warning("Cannot get item.") + continue + # Add Cargo to the fitting + c = Cargo(item) + c.amount = cargoAmount + fitobj.cargo.append(c) + # 2017/03/27 NOTE: store description from EFT + elif description: + fitobj.notes = description.group(1).replace("|", "\n") + else: + withCharge = re.match("(.+),(.+)", 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 + try: + modItem = sMkt.getItem(modName) + except: + pyfalog.warning("Cannot get item.") + continue + + # Create module + m = Module(modItem) + + # Add subsystems before modules to make sure T3 cruisers have subsystems installed + if modItem.category.name == "Subsystem": + if m.fits(fitobj): + fitobj.modules.append(m) + else: + m.owner = fitobj + # Activate mod if it is activable + if m.isValidState(State.ACTIVE): + m.state = State.ACTIVE + # Add charge to mod if applicable, on any errors just don't add anything + if chargeName: + try: + chargeItem = sMkt.getItem(chargeName, eager="group.category") + if chargeItem.category.name == "Charge": + m.charge = chargeItem + except: + pyfalog.warning("Cannot get item.") + pass + # Append module to fit + moduleList.append(m) + + # Recalc to get slot numbers correct for T3 cruisers + svcFit.getInstance().recalc(fitobj) + + for module in moduleList: + if module.fits(fitobj): + fitobj.modules.append(module) + + # 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) + ) + + # Skip fit silently if we get an exception + except Exception as e: + pyfalog.error("Caught exception on fit.") + pyfalog.error(e) + pass + + return fits + + +def _importPrepareString(eftString): + lines = eftString.splitlines() + for i in range(len(lines)): + lines[i] = lines[i].strip() + while lines and not lines[0]: + del lines[0] + while lines and not lines[-1]: + del lines[-1] + return lines + + +def _importGetMutationData(lines): + data = {} + consumedIndices = set() + for i in range(len(lines)): + line = lines[i] + m = re.match('^\[(?P\d+)\]', line) + if m: + ref = int(m.group('ref')) + # Attempt to apply mutation is useless w/o mutaplasmid, so skip it + # altogether if we have no info on it + try: + mutaName = lines[i + 1] + except IndexError: + continue + else: + consumedIndices.add(i) + consumedIndices.add(i + 1) + # Get custom attribute values + mutaAttrs = {} + try: + mutaAttrsLine = lines[i + 2] + except IndexError: + pass + else: + consumedIndices.add(i + 2) + pairs = [p.strip() for p in mutaAttrsLine.split(',')] + for pair in pairs: + try: + attrName, value = pair.split(' ') + except ValueError: + continue + try: + value = float(value) + except (ValueError, TypeError): + continue + attrInfo = getAttributeInfo(attrName.strip()) + if attrInfo is None: + continue + mutaAttrs[attrInfo.ID] = value + mutaItem = _fetchItem(mutaName) + if mutaItem is None: + continue + data[ref] = (mutaItem, mutaAttrs) + # If we got here, we have seen at least correct reference line and + # mutaplasmid name line + i += 2 + # Bonus points for seeing correct attrs line. Worst case we + # will have to scan it once again + if mutaAttrs: + i += 1 + # Cleanup the lines from mutaplasmid info + for i in sorted(consumedIndices, reverse=True): + del lines[i] + return data + + +def _importSectionIter(lines): + section = Section() + for line in lines: + if not line: + if section.lines: + yield section + section = Section() + else: + section.lines.append(line) + if section.lines: + yield section + + +def _importCreateFit(lines): + """Create fit and set top-level entity (ship or citadel).""" + fit = Fit() + header = lines.pop(0) + m = re.match('\[(?P[\w\s]+),\s*(?P.+)\]', header) + if not m: + pyfalog.warning('service.port.eft.importEft: corrupted fit header') + raise EftImportError + shipType = m.group('shipType').strip() + fitName = m.group('fitName').strip() + try: + ship = _fetchItem(shipType) + try: + fit.ship = Ship(ship) + except ValueError: + fit.ship = Citadel(ship) + fit.name = fitName + except: + pyfalog.warning('service.port.eft.importEft: exception caught when parsing header') + raise EftImportError + return fit + + +def _fetchItem(typeName, eagerCat=False): + sMkt = Market.getInstance() + eager = 'group.category' if eagerCat else None + try: + item = sMkt.getItem(typeName, eager=eager) + except: + pyfalog.warning('service.port.eft: unable to fetch item "{}"'.format(typeName)) + return None + if sMkt.getPublicityByItem(item): + return item + else: + return None + + +def _clearTail(lst): + while lst and lst[-1] is None: + del lst[-1] + + +class EftImportError(Exception): + """Exception class emitted and consumed by EFT importer internally.""" + ... + + +class Section: + + def __init__(self): + self.lines = [] + self.itemSpecs = [] + self.__itemDataCats = None + + @property + def itemDataCats(self): + if self.__itemDataCats is None: + cats = set() + for itemSpec in self.itemSpecs: + if itemSpec is None: + continue + cats.add(itemSpec.item.category.name) + self.__itemDataCats = tuple(sorted(cats)) + return self.__itemDataCats + + @property + def isModuleRack(self): + return all(i is None or i.isModule for i in self.itemSpecs) + + @property + def isImplantRack(self): + return all(i is not None and i.isImplant for i in self.itemSpecs) + + @property + def isDroneBay(self): + return all(i is not None and i.isDrone for i in self.itemSpecs) + + @property + def isFighterBay(self): + return all(i is not None and i.isFighter for i in self.itemSpecs) + + @property + def isCargoHold(self): + return ( + all(i is not None and i.isCargo for i in self.itemSpecs) and + not self.isDroneBay and not self.isFighterBay) + + +class BaseItemSpec: + + def __init__(self, typeName): + item = _fetchItem(typeName, eagerCat=True) + if item is None: + raise EftImportError + self.typeName = typeName + self.item = item + + @property + def isModule(self): + return False + + @property + def isImplant(self): + return False + + @property + def isDrone(self): + return False + + @property + def isFighter(self): + return False + + @property + def isCargo(self): + return False + + +class RegularItemSpec(BaseItemSpec): + + def __init__(self, typeName, chargeName=None): + super().__init__(typeName) + self.charge = self.__fetchCharge(chargeName) + self.offline = False + self.mutationIdx = None + + def __fetchCharge(self, chargeName): + if chargeName: + charge = _fetchItem(chargeName, eagerCat=True) + if not charge or charge.category.name != 'Charge': + charge = None + else: + charge = None + return charge + + @property + def isModule(self): + return self.item.category.name in MODULE_CATS + + @property + def isImplant(self): + return ( + self.item.category.name == 'Implant' and ( + 'implantness' in self.item.attributes or + 'boosterness' in self.item.attributes)) + + +class MultiItemSpec(BaseItemSpec): + + def __init__(self, typeName): + super().__init__(typeName) + self.amount = 0 + + @property + def isDrone(self): + return self.item.category.name == 'Drone' + + @property + def isFighter(self): + return self.item.category.name == 'Fighter' + + @property + def isCargo(self): + return True + + +class AbstractFit: + + def __init__(self): + # Modules + self.modulesHigh = [] + self.modulesMed = [] + self.modulesLow = [] + self.rigs = [] + self.subsystems = [] + self.services = [] + # Non-modules + self.implants = [] + self.boosters = [] + self.drones = {} # Format: {item: Drone} + self.fighters = [] + self.cargo = {} # Format: {item: Cargo} + # Other stuff + self.mutations = {} # Format: {reference: (mutaplamid item, {attr ID: attr value})} + + @property + def __slotContainerMap(self): + return { + Slot.HIGH: self.modulesHigh, + Slot.MED: self.modulesMed, + Slot.LOW: self.modulesLow, + Slot.RIG: self.rigs, + Slot.SUBSYSTEM: self.subsystems, + Slot.SERVICE: self.services} + + def getContainerBySlot(self, slotType): + return self.__slotContainerMap.get(slotType) + + def getSlotByContainer(self, container): + slotType = None + for k, v in self.__slotContainerMap.items(): + if v is container: + slotType = k + break + return slotType + + def addModules(self, itemSpecs): + modules = [] + slotTypes = set() + for itemSpec in itemSpecs: + if itemSpec is None: + modules.append(None) + continue + m = self.__makeModule(itemSpec) + if m is None: + modules.append(None) + continue + modules.append(m) + slotTypes.add(m.slot) + _clearTail(modules) + # If all the modules have same slot type, put them to appropriate + # container with stubs + if len(slotTypes) == 1: + slotType = tuple(slotTypes)[0] + self.getContainerBySlot(slotType).extend(modules) + # Otherwise, put just modules + else: + for m in modules: + if m is None: + continue + self.getContainerBySlot(m.slot).append(m) + + def addModule(self, itemSpec): + if itemSpec is None: + return + m = self.__makeModule(itemSpec) + if m is not None: + self.getContainerBySlot(m.slot).append(m) + + def __makeModule(self, itemSpec): + # Mutate item if needed + m = None + if itemSpec.mutationIdx in self.mutations: + mutaItem, mutaAttrs = self.mutations[itemSpec.mutationIdx] + mutaplasmid = getDynamicItem(mutaItem.ID) + if mutaplasmid: + try: + m = Module(mutaplasmid.resultingItem, itemSpec.item, mutaplasmid) + except ValueError: + pass + else: + for attrID, mutator in m.mutators.items(): + if attrID in mutaAttrs: + mutator.value = mutaAttrs[attrID] + # If we still don't have item (item is not mutated or we + # failed to construct mutated item), try to make regular item + if m is None: + try: + m = Module(itemSpec.item) + except ValueError: + return None + + if itemSpec.charge is not None and m.isValidCharge(itemSpec.charge): + m.charge = itemSpec.charge + if itemSpec.offline and m.isValidState(State.OFFLINE): + m.state = State.OFFLINE + elif m.isValidState(State.ACTIVE): + m.state = State.ACTIVE + return m + + def addImplant(self, itemSpec): + if itemSpec is None: + return + if 'implantness' in itemSpec.item.attributes: + self.implants.append(Implant(itemSpec.item)) + elif 'boosterness' in itemSpec.item.attributes: + self.boosters.append(Booster(itemSpec.item)) + else: + pyfalog.error('Failed to import implant: {}', itemSpec.typeName) + + def addDrone(self, itemSpec): + if itemSpec is None: + return + if itemSpec.item not in self.drones: + self.drones[itemSpec.item] = Drone(itemSpec.item) + self.drones[itemSpec.item].amount += itemSpec.amount + + def addFighter(self, itemSpec): + if itemSpec is None: + return + fighter = Fighter(itemSpec.item) + fighter.amount = itemSpec.amount + self.fighters.append(fighter) + + def addCargo(self, itemSpec): + if itemSpec is None: + return + if itemSpec.item not in self.cargo: + self.cargo[itemSpec.item] = Cargo(itemSpec.item) + self.cargo[itemSpec.item].amount += itemSpec.amount diff --git a/service/port/esi.py b/service/port/esi.py new file mode 100644 index 000000000..f1e02d13a --- /dev/null +++ b/service/port/esi.py @@ -0,0 +1,208 @@ +# ============================================================================= +# Copyright (C) 2014 Ryan Holmes +# +# 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 . +# ============================================================================= + + +import collections +import json + +from logbook import Logger + +from eos.saveddata.cargo import Cargo +from eos.saveddata.citadel import Citadel +from eos.saveddata.drone import Drone +from eos.saveddata.fighter import Fighter +from eos.saveddata.fit import Fit +from eos.saveddata.module import Module, State, Slot +from eos.saveddata.ship import Ship +from service.fit import Fit as svcFit +from service.market import Market + + +class ESIExportException(Exception): + pass + + +pyfalog = Logger(__name__) + +INV_FLAGS = { + Slot.LOW: 11, + Slot.MED: 19, + Slot.HIGH: 27, + Slot.RIG: 92, + Slot.SUBSYSTEM: 125, + Slot.SERVICE: 164 +} + +INV_FLAG_CARGOBAY = 5 +INV_FLAG_DRONEBAY = 87 +INV_FLAG_FIGHTER = 158 + + +def exportESI(ofit): + # A few notes: + # max fit name length is 50 characters + # Most keys are created simply because they are required, but bogus data is okay + + nested_dict = lambda: collections.defaultdict(nested_dict) + fit = nested_dict() + sFit = svcFit.getInstance() + + # max length is 50 characters + name = ofit.name[:47] + '...' if len(ofit.name) > 50 else ofit.name + fit['name'] = name + fit['ship_type_id'] = ofit.ship.item.ID + + # 2017/03/29 NOTE: "<" or "<" is Ignored + # fit['description'] = "" % ofit.ID + fit['description'] = ofit.notes[:397] + '...' if len(ofit.notes) > 400 else ofit.notes if ofit.notes is not None else "" + fit['items'] = [] + + slotNum = {} + charges = {} + for module in ofit.modules: + if module.isEmpty: + continue + + item = nested_dict() + slot = module.slot + + if slot == Slot.SUBSYSTEM: + # Order of subsystem matters based on this attr. See GH issue #130 + slot = int(module.getModifiedItemAttr("subSystemSlot")) + item['flag'] = slot + else: + if slot not in slotNum: + slotNum[slot] = INV_FLAGS[slot] + + item['flag'] = slotNum[slot] + slotNum[slot] += 1 + + item['quantity'] = 1 + item['type_id'] = module.item.ID + fit['items'].append(item) + + if module.charge and sFit.serviceFittingOptions["exportCharges"]: + if module.chargeID not in charges: + charges[module.chargeID] = 0 + # `or 1` because some charges (ie scripts) are without qty + charges[module.chargeID] += module.numCharges or 1 + + for cargo in ofit.cargo: + item = nested_dict() + item['flag'] = INV_FLAG_CARGOBAY + item['quantity'] = cargo.amount + item['type_id'] = cargo.item.ID + fit['items'].append(item) + + for chargeID, amount in list(charges.items()): + item = nested_dict() + item['flag'] = INV_FLAG_CARGOBAY + item['quantity'] = amount + item['type_id'] = chargeID + fit['items'].append(item) + + for drone in ofit.drones: + item = nested_dict() + item['flag'] = INV_FLAG_DRONEBAY + item['quantity'] = drone.amount + item['type_id'] = drone.item.ID + fit['items'].append(item) + + for fighter in ofit.fighters: + item = nested_dict() + item['flag'] = INV_FLAG_FIGHTER + item['quantity'] = fighter.amountActive + item['type_id'] = fighter.item.ID + fit['items'].append(item) + + if len(fit['items']) == 0: + raise ESIExportException("Cannot export fitting: module list cannot be empty.") + + return json.dumps(fit) + + +def importESI(str_): + + sMkt = Market.getInstance() + fitobj = Fit() + refobj = json.loads(str_) + items = refobj['items'] + # "<" and ">" is replace to "<", ">" by EVE client + fitobj.name = refobj['name'] + # 2017/03/29: read description + fitobj.notes = refobj['description'] + + try: + ship = refobj['ship_type_id'] + try: + fitobj.ship = Ship(sMkt.getItem(ship)) + except ValueError: + fitobj.ship = Citadel(sMkt.getItem(ship)) + except: + pyfalog.warning("Caught exception in importESI") + return None + + items.sort(key=lambda k: k['flag']) + + moduleList = [] + for module in items: + try: + item = sMkt.getItem(module['type_id'], eager="group.category") + if not item.published: + continue + if module['flag'] == INV_FLAG_DRONEBAY: + d = Drone(item) + d.amount = module['quantity'] + fitobj.drones.append(d) + elif module['flag'] == INV_FLAG_CARGOBAY: + c = Cargo(item) + c.amount = module['quantity'] + fitobj.cargo.append(c) + elif module['flag'] == INV_FLAG_FIGHTER: + fighter = Fighter(item) + fitobj.fighters.append(fighter) + else: + try: + m = Module(item) + # When item can't be added to any slot (unknown item or just charge), ignore it + except ValueError: + pyfalog.debug("Item can't be added to any slot (unknown item or just charge)") + continue + # Add subsystems before modules to make sure T3 cruisers have subsystems installed + if item.category.name == "Subsystem": + if m.fits(fitobj): + fitobj.modules.append(m) + else: + if m.isValidState(State.ACTIVE): + m.state = State.ACTIVE + + moduleList.append(m) + + except: + pyfalog.warning("Could not process module.") + continue + + # Recalc to get slot numbers correct for T3 cruisers + svcFit.getInstance().recalc(fitobj) + + for module in moduleList: + if module.fits(fitobj): + fitobj.modules.append(module) + + return fitobj diff --git a/service/port/multibuy.py b/service/port/multibuy.py new file mode 100644 index 000000000..2bf2d7a8a --- /dev/null +++ b/service/port/multibuy.py @@ -0,0 +1,68 @@ +# ============================================================================= +# Copyright (C) 2014 Ryan Holmes +# +# 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 . +# ============================================================================= + + +from service.fit import Fit as svcFit +from service.port.eft import SLOT_ORDER as EFT_SLOT_ORDER + + +def exportMultiBuy(fit): + export = "%s\n" % fit.ship.item.name + stuff = {} + sFit = svcFit.getInstance() + for module in fit.modules: + slot = module.slot + if slot not in stuff: + stuff[slot] = [] + curr = "%s\n" % module.item.name if module.item else "" + if module.charge and sFit.serviceFittingOptions["exportCharges"]: + curr += "%s x%s\n" % (module.charge.name, module.numCharges) + stuff[slot].append(curr) + + for slotType in EFT_SLOT_ORDER: + data = stuff.get(slotType) + if data is not None: + # export += "\n" + for curr in data: + export += curr + + if len(fit.drones) > 0: + for drone in fit.drones: + export += "%s x%s\n" % (drone.item.name, drone.amount) + + if len(fit.cargo) > 0: + for cargo in fit.cargo: + export += "%s x%s\n" % (cargo.item.name, cargo.amount) + + if len(fit.implants) > 0: + for implant in fit.implants: + export += "%s\n" % implant.item.name + + if len(fit.boosters) > 0: + for booster in fit.boosters: + export += "%s\n" % booster.item.name + + if len(fit.fighters) > 0: + for fighter in fit.fighters: + export += "%s x%s\n" % (fighter.item.name, fighter.amountActive) + + if export[-1] == "\n": + export = export[:-1] + + return export diff --git a/service/port/port.py b/service/port/port.py new file mode 100644 index 000000000..560af14ea --- /dev/null +++ b/service/port/port.py @@ -0,0 +1,277 @@ +# ============================================================================= +# Copyright (C) 2014 Ryan Holmes +# +# 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 . +# ============================================================================= + + +import re +import os +import threading +import xml.dom +import xml.parsers.expat +from codecs import open + +from bs4 import UnicodeDammit +from logbook import Logger + +from eos import db +from eos.saveddata.fit import ImplantLocation +from service.fit import Fit as svcFit +from service.port.dna import exportDna, importDna +from service.port.eft import exportEft, importEft, importEftCfg +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.xml import importXml, exportXml + + +pyfalog = Logger(__name__) + +# 2017/04/05 NOTE: simple validation, for xml file +RE_XML_START = r'<\?xml\s+version="1.0"\s*\?>' + + +class Port(object): + """Service which houses all import/export format functions""" + instance = None + __tag_replace_flag = True + + @classmethod + def getInstance(cls): + if cls.instance is None: + cls.instance = Port() + + return cls.instance + + @classmethod + def set_tag_replace(cls, b): + cls.__tag_replace_flag = b + + @classmethod + def is_tag_replace(cls): + # might there is a person who wants to hold tags. + # (item link in EVE client etc. When importing again to EVE) + return cls.__tag_replace_flag + + @staticmethod + def backupFits(path, iportuser): + pyfalog.debug("Starting backup fits thread.") + + def backupFitsWorkerFunc(path, iportuser): + success = True + try: + iportuser.on_port_process_start() + backedUpFits = Port.exportXml(iportuser, + *svcFit.getInstance().getAllFits()) + 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.") + + threading.Thread( + target=backupFitsWorkerFunc, + args=(path, iportuser) + ).start() + + @staticmethod + def importFitsThreaded(paths, iportuser): + # type: (tuple, IPortUser) -> None + """ + :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) + + threading.Thread( + target=importFitsFromFileWorkerFunc, + args=(paths, iportuser) + ).start() + + @staticmethod + def importFitFromFiles(paths, iportuser=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 + fits are processed as well as when fits are being saved. + returns + """ + + sFit = svcFit.getInstance() + + fit_list = [] + try: + for path in paths: + if iportuser: # Pulse + msg = "Processing file:\n%s" % path + 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() + dammit = UnicodeDammit(srcString) + srcString = dammit.unicode_markup + + if len(srcString) == 0: # ignore blank files + pyfalog.debug("File is blank.") + continue + + try: + _, fitsImport = Port.importAuto(srcString, path, iportuser=iportuser) + fit_list += fitsImport + except xml.parsers.expat.ExpatError: + pyfalog.warning("Malformed XML in:\n{0}", path) + return False, "Malformed XML in %s" % path + + # IDs = [] # NOTE: what use for IDs? + numFits = len(fit_list) + for idx, fit in enumerate(fit_list): + # Set some more fit attributes and save + fit.character = sFit.character + fit.damagePattern = sFit.pattern + fit.targetResists = sFit.targetResists + if len(fit.implants) > 0: + fit.implantLocation = ImplantLocation.FIT + else: + useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"] + fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT + db.save(fit) + # IDs.append(fit.ID) + if iportuser: # Pulse + 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" + except Exception as e: + pyfalog.critical("Unknown exception processing: {0}", path) + pyfalog.critical(e) + # TypeError: not all arguments converted during string formatting +# return False, "Unknown Error while processing {0}" % path + return False, "Unknown error while processing %s\n\n Error: %s" % (path, e.message) + + return True, fit_list + + @staticmethod + def importFitFromBuffer(bufferStr, activeFit=None): + # type: (str, object) -> object + # TODO: catch the exception? + # activeFit is reserved?, bufferStr is unicode? (assume only clipboard string? + sFit = svcFit.getInstance() + _, fits = Port.importAuto(bufferStr, activeFit=activeFit) + for fit in fits: + fit.character = sFit.character + fit.damagePattern = sFit.pattern + fit.targetResists = sFit.targetResists + if len(fit.implants) > 0: + fit.implantLocation = ImplantLocation.FIT + else: + useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"] + fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT + db.save(fit) + return fits + + @classmethod + def importAuto(cls, string, path=None, activeFit=None, iportuser=None): + # type: (Port, str, str, object, IPortUser) -> object + # 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() + + # If XML-style start of tag encountered, detect as XML + if re.search(RE_XML_START, firstLine): + return "XML", cls.importXml(string, iportuser) + + # If JSON-style start, parse as CREST/JSON + if firstLine[0] == '{': + return "JSON", (cls.importESI(string),) + + # 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("\[.*\]", firstLine) and path is not None: + filename = os.path.split(path)[1] + shipName = filename.rsplit('.')[0] + return "EFT Config", cls.importEftCfg(shipName, string, 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),) + + # Use DNA format for all other cases + return "DNA", (cls.importDna(string),) + + # EFT-related methods + @staticmethod + def importEft(eftString): + return importEft(eftString) + + @staticmethod + def importEftCfg(shipname, contents, iportuser=None): + return importEftCfg(shipname, contents, iportuser) + + @classmethod + def exportEft(cls, fit, options): + return exportEft(fit, options) + + # DNA-related methods + @staticmethod + def importDna(string): + return importDna(string) + + @staticmethod + def exportDna(fit): + return exportDna(fit) + + # ESI-related methods + @staticmethod + def importESI(string): + return importESI(string) + + @staticmethod + def exportESI(fit): + return exportESI(fit) + + # XML-related methods + @staticmethod + def importXml(text, iportuser=None): + return importXml(text, iportuser) + + @staticmethod + def exportXml(iportuser=None, *fits): + return exportXml(iportuser, *fits) + + # Multibuy-related methods + @staticmethod + def exportMultiBuy(fit): + return exportMultiBuy(fit) diff --git a/service/port/shared.py b/service/port/shared.py new file mode 100644 index 000000000..a21a81d63 --- /dev/null +++ b/service/port/shared.py @@ -0,0 +1,70 @@ +# ============================================================================= +# Copyright (C) 2014 Ryan Holmes +# +# 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 . +# ============================================================================= + + +from abc import ABCMeta, abstractmethod + + +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 diff --git a/service/port/xml.py b/service/port/xml.py new file mode 100644 index 000000000..54d5a98eb --- /dev/null +++ b/service/port/xml.py @@ -0,0 +1,326 @@ +# ============================================================================= +# Copyright (C) 2014 Ryan Holmes +# +# 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 . +# ============================================================================= + +import re +import xml.dom +import xml.parsers.expat + +from logbook import Logger + +from eos.saveddata.cargo import Cargo +from eos.saveddata.citadel import Citadel +from eos.saveddata.drone import Drone +from eos.saveddata.fighter import Fighter +from eos.saveddata.fit import Fit +from eos.saveddata.module import Module, State, Slot +from eos.saveddata.ship import Ship +from service.fit import Fit as svcFit +from service.market import Market +from utils.strfunctions import sequential_rep, replace_ltgt + +from service.port.shared import IPortUser, processing_notify + + +pyfalog = Logger(__name__) + +# -- 170327 Ignored description -- +RE_LTGT = "&(lt|gt);" +L_MARK = "<localized hint="" +# <localized hint="([^"]+)">([^\*]+)\*<\/localized> +LOCALIZED_PATTERN = re.compile(r'([^\*]+)\*') + + +def _extract_match(t): + m = LOCALIZED_PATTERN.match(t) + # hint attribute, text content + return m.group(1), m.group(2) + + +def _resolve_ship(fitting, sMkt, b_localized): + # type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.fit.Fit + """ NOTE: Since it is meaningless unless a correct ship object can be constructed, + process flow changed + """ + # ------ Confirm ship + # Maelstrom + shipType = fitting.getElementsByTagName("shipType").item(0).getAttribute("value") + anything = None + if b_localized: + # expect an official name, emergency cache + shipType, anything = _extract_match(shipType) + + limit = 2 + ship = None + while True: + must_retry = False + try: + try: + ship = Ship(sMkt.getItem(shipType)) + except ValueError: + ship = Citadel(sMkt.getItem(shipType)) + except Exception as e: + pyfalog.warning("Caught exception on _resolve_ship") + pyfalog.error(e) + limit -= 1 + if limit is 0: + break + shipType = anything + must_retry = True + if not must_retry: + break + + if ship is None: + raise Exception("cannot resolve ship type.") + + fitobj = Fit(ship=ship) + # ------ Confirm fit name + anything = fitting.getAttribute("name") + # 2017/03/29 NOTE: + # if fit name contained "<" or ">" then reprace to named html entity by EVE client + # if re.search(RE_LTGT, anything): + if "<" in anything or ">" in anything: + anything = replace_ltgt(anything) + fitobj.name = anything + + return fitobj + + +def _resolve_module(hardware, sMkt, b_localized): + # type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.module.Module + moduleName = hardware.getAttribute("type") + emergency = None + if b_localized: + # expect an official name, emergency cache + moduleName, emergency = _extract_match(moduleName) + + item = None + limit = 2 + while True: + must_retry = False + try: + item = sMkt.getItem(moduleName, eager="group.category") + except Exception as e: + pyfalog.warning("Caught exception on _resolve_module") + pyfalog.error(e) + limit -= 1 + if limit is 0: + break + moduleName = emergency + must_retry = True + if not must_retry: + break + return item + + +def importXml(text, iportuser): + from .port import Port + # type: (str, IPortUser) -> list[eos.saveddata.fit.Fit] + sMkt = Market.getInstance() + doc = xml.dom.minidom.parseString(text) + # NOTE: + # When L_MARK is included at this point, + # Decided to be localized data + b_localized = L_MARK in text + fittings = doc.getElementsByTagName("fittings").item(0) + fittings = fittings.getElementsByTagName("fitting") + fit_list = [] + failed = 0 + + for fitting in fittings: + try: + fitobj = _resolve_ship(fitting, sMkt, b_localized) + except: + failed += 1 + continue + + # -- 170327 Ignored description -- + # read description from exported xml. (EVE client, EFT) + description = fitting.getElementsByTagName("description").item(0).getAttribute("value") + if description is None: + description = "" + elif len(description): + # convert
to "\n" and remove html tags. + if Port.is_tag_replace(): + description = replace_ltgt( + sequential_rep(description, r"<(br|BR)>", "\n", r"<[^<>]+>", "") + ) + fitobj.notes = description + + hardwares = fitting.getElementsByTagName("hardware") + moduleList = [] + for hardware in hardwares: + try: + item = _resolve_module(hardware, sMkt, b_localized) + if not item or not item.published: + continue + + if item.category.name == "Drone": + d = Drone(item) + d.amount = int(hardware.getAttribute("qty")) + fitobj.drones.append(d) + elif item.category.name == "Fighter": + ft = Fighter(item) + ft.amount = int(hardware.getAttribute("qty")) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize + fitobj.fighters.append(ft) + elif hardware.getAttribute("slot").lower() == "cargo": + # although the eve client only support charges in cargo, third-party programs + # may support items or "refits" in cargo. Support these by blindly adding all + # cargo, not just charges + c = Cargo(item) + c.amount = int(hardware.getAttribute("qty")) + fitobj.cargo.append(c) + else: + try: + 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") + continue + # Add subsystems before modules to make sure T3 cruisers have subsystems installed + if item.category.name == "Subsystem": + if m.fits(fitobj): + m.owner = fitobj + fitobj.modules.append(m) + else: + if m.isValidState(State.ACTIVE): + m.state = State.ACTIVE + + moduleList.append(m) + + except KeyboardInterrupt: + pyfalog.warning("Keyboard Interrupt") + continue + + # Recalc to get slot numbers correct for T3 cruisers + svcFit.getInstance().recalc(fitobj) + + for module in moduleList: + if module.fits(fitobj): + module.owner = fitobj + 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) + ) + + return fit_list + + +def exportXml(iportuser, *fits): + doc = xml.dom.minidom.Document() + fittings = doc.createElement("fittings") + # fit count + fit_count = len(fits) + fittings.setAttribute("count", "%s" % fit_count) + doc.appendChild(fittings) + sFit = svcFit.getInstance() + + for i, fit in enumerate(fits): + try: + fitting = doc.createElement("fitting") + fitting.setAttribute("name", fit.name) + fittings.appendChild(fitting) + description = doc.createElement("description") + # -- 170327 Ignored description -- + try: + notes = fit.notes # unicode + + if notes: + notes = notes[:397] + '...' if len(notes) > 400 else notes + + description.setAttribute( + "value", re.sub("(\r|\n|\r\n)+", "
", notes) if notes is not None else "" + ) + except Exception as e: + pyfalog.warning("read description is failed, msg=%s\n" % e.args) + + fitting.appendChild(description) + shipType = doc.createElement("shipType") + shipType.setAttribute("value", fit.ship.name) + fitting.appendChild(shipType) + + charges = {} + slotNum = {} + for module in fit.modules: + if module.isEmpty: + continue + + slot = module.slot + + if slot == Slot.SUBSYSTEM: + # Order of subsystem matters based on this attr. See GH issue #130 + slotId = module.getModifiedItemAttr("subSystemSlot") - 125 + else: + if slot not in slotNum: + slotNum[slot] = 0 + + slotId = slotNum[slot] + slotNum[slot] += 1 + + hardware = doc.createElement("hardware") + hardware.setAttribute("type", module.item.name) + slotName = Slot.getName(slot).lower() + slotName = slotName if slotName != "high" else "hi" + hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId)) + fitting.appendChild(hardware) + + if module.charge and sFit.serviceFittingOptions["exportCharges"]: + if module.charge.name not in charges: + charges[module.charge.name] = 0 + # `or 1` because some charges (ie scripts) are without qty + charges[module.charge.name] += module.numCharges or 1 + + for drone in fit.drones: + hardware = doc.createElement("hardware") + hardware.setAttribute("qty", "%d" % drone.amount) + hardware.setAttribute("slot", "drone bay") + hardware.setAttribute("type", drone.item.name) + fitting.appendChild(hardware) + + for fighter in fit.fighters: + hardware = doc.createElement("hardware") + hardware.setAttribute("qty", "%d" % fighter.amountActive) + hardware.setAttribute("slot", "fighter bay") + hardware.setAttribute("type", fighter.item.name) + fitting.appendChild(hardware) + + for cargo in fit.cargo: + if cargo.item.name not in charges: + charges[cargo.item.name] = 0 + charges[cargo.item.name] += cargo.amount + + for name, qty in list(charges.items()): + hardware = doc.createElement("hardware") + hardware.setAttribute("qty", "%d" % qty) + hardware.setAttribute("slot", "cargo") + hardware.setAttribute("type", name) + fitting.appendChild(hardware) + 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)) + ) + return doc.toprettyxml()