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