#=============================================================================== # 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 eos.types import State, Slot, Module, Cargo, Fit, Ship, Drone, Implant, Booster import service import wx import logging logger = logging.getLogger("pyfa.service.port") try: from collections import OrderedDict except ImportError: from utils.compat import OrderedDict EFT_SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM] class Port(object): """Service which houses all import/export format functions""" @classmethod def importAuto(cls, string, path=None, activeFit=None, callback=None, encoding=None): # 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.match("<", firstLine): return "XML", cls.importXml(string, callback, encoding) # 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, callback) # 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 importDna(string): sMkt = service.Market.getInstance() info = string.split(":") f = Fit() try: f.ship = Ship(sMkt.getItem(int(info[0]))) f.name = "{0} - DNA Imported".format(f.ship.item.name) except UnicodeEncodeError as e: def logtransform(s): if len(s) > 10: return s[:10] + "..." return s logger.exception("Couldn't import ship data %r", [ logtransform(s) for s in info ]) return None 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 == "Charge": c = Cargo(item) c.amount = int(amount) f.cargo.append(c) else: for i in xrange(int(amount)): try: m = Module(item) f.modules.append(m) except: pass if m.isValidState(State.ACTIVE): m.state = State.ACTIVE return f @staticmethod def importEft(eftString): sMkt = service.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) fit.ship = Ship(ship) fit.name = fitName except: return # maintain map of drones and their quantities droneMap = {} cargoMap = {} 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) continue if item.category.name == "Drone": extraAmount = int(extraAmount) if extraAmount is not None else 1 if not modName in droneMap: droneMap[modName] = 0 droneMap[modName] += extraAmount if len(modExtra) == 2 and item.category.name != "Drone": extraAmount = int(extraAmount) if extraAmount is not None else 1 if not modName in cargoMap: cargoMap[modName] = 0 cargoMap[modName] += extraAmount elif item.category.name == "Implant": fit.implants.append(Implant(item)) else: try: m = Module(item) except ValueError: continue 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 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, callback=None): """Handle import from EFT config store file""" # Check if we have such ship in database, bail if we don't sMkt = service.Market.getInstance() try: sMkt.getItem(shipname) except: return [] # empty list is expected # If client didn't take care of encoding file contents into Unicode, # do it using fallback encoding ourselves if isinstance(contents, str): contents = unicode(contents, "cp1252") 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 f = Fit() # Strip square brackets and pull out a fit name f.name = fitLines[0][1:-1] # Assign ship to fitting f.ship = Ship(sMkt.getItem(shipname)) 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) 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: continue if droneItem.category.name != "Drone": continue # Add drone to the fitting d = Drone(droneItem) d.amount = droneAmount if entityState == "Active": d.amountActive = droneAmount elif entityState == "Inactive": d.amountActive = 0 f.drones.append(d) 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: 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 f.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: 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 f.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: continue # Add Cargo to the fitting c = Cargo(item) c.amount = cargoAmount f.cargo.append(c) 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: continue # Create module and activate it if it's activable m = Module(modItem) 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: pass # Append module to fit f.modules.append(m) # Append fit to list of fits fits.append(f) if callback: wx.CallAfter(callback, None) # Skip fit silently if we get an exception except Exception: pass return fits @staticmethod def importXml(text, callback=None, encoding="utf-8"): sMkt = service.Market.getInstance() doc = xml.dom.minidom.parseString(text.encode(encoding)) fittings = doc.getElementsByTagName("fittings").item(0) fittings = fittings.getElementsByTagName("fitting") fits = [] for i, fitting in enumerate(fittings): f = Fit() f.name = fitting.getAttribute("name") # Maelstrom shipType = fitting.getElementsByTagName("shipType").item(0).getAttribute("value") try: f.ship = Ship(sMkt.getItem(shipType)) except: continue hardwares = fitting.getElementsByTagName("hardware") for hardware in hardwares: try: moduleName = hardware.getAttribute("type") try: item = sMkt.getItem(moduleName, eager="group.category") except: continue if item: if item.category.name == "Drone": d = Drone(item) d.amount = int(hardware.getAttribute("qty")) f.drones.append(d) 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")) f.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: continue if m.isValidState(State.ACTIVE): m.state = State.ACTIVE f.modules.append(m) except KeyboardInterrupt: continue fits.append(f) if callback: wx.CallAfter(callback, None) return fits @staticmethod def _exportEftBase(fit): offineSuffix = " /OFFLINE" export = "[%s, %s]\n" % (fit.ship.item.name, fit.name) stuff = {} for module in fit.modules: slot = module.slot if not slot in stuff: stuff[slot] = [] curr = module.item.name if module.item else ("[Empty %s slot]" % Slot.getName(slot).capitalize() if slot is not None else "") if module.charge: curr += ", %s" % module.charge.name if module.state == State.OFFLINE: curr += offineSuffix curr += "\n" 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: export += "\n\n" for drone in fit.drones: export += "%s x%s\n" % (drone.item.name, drone.amount) if export[-1] == "\n": export = export[:-1] return export @classmethod def exportEft(cls, fit): export = cls._exportEftBase(fit) if len(fit.cargo) > 0: export += "\n\n\n" for cargo in fit.cargo: export += "%s x%s\n" % (cargo.item.name, cargo.amount) if export[-1] == "\n": export = export[:-1] return export @classmethod def exportEftImps(cls, fit): export = cls._exportEftBase(fit) if len(fit.implants) > 0: export += "\n\n\n" for implant in fit.implants: export += "%s\n" % implant.item.name if export[-1] == "\n": export = export[:-1] if len(fit.cargo) > 0: export += "\n\n\n" for cargo in fit.cargo: export += "%s x%s\n" % (cargo.item.name, cargo.amount) if export[-1] == "\n": export = export[:-1] return export @staticmethod def exportDna(fit): dna = str(fit.shipID) subsystems = [] # EVE cares which order you put these in mods = OrderedDict() charges = OrderedDict() for mod in fit.modules: if not mod.isEmpty: if mod.slot == Slot.SUBSYSTEM: subsystems.append(mod) continue if not mod.itemID in mods: mods[mod.itemID] = 0 mods[mod.itemID] += 1 if mod.charge: if not mod.chargeID 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 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 not cargo.item.ID 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 + "::" @classmethod def exportXml(cls, callback=None, *fits): doc = xml.dom.minidom.Document() fittings = doc.createElement("fittings") doc.appendChild(fittings) for i, fit in enumerate(fits): try: fitting = doc.createElement("fitting") fitting.setAttribute("name", fit.name) fittings.appendChild(fitting) description = doc.createElement("description") description.setAttribute("value", "") fitting.appendChild(description) shipType = doc.createElement("shipType") shipType.setAttribute("value", fit.ship.item.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") - 124 else: if not slot 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: if not module.charge.name 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 cargo in fit.cargo: if not cargo.item.name in charges: charges[cargo.item.name] = 0 charges[cargo.item.name] += cargo.amount for name, qty in charges.items(): hardware = doc.createElement("hardware") hardware.setAttribute("qty", "%d" % qty) hardware.setAttribute("slot", "cargo") hardware.setAttribute("type", name) fitting.appendChild(hardware) except: print "Failed on fitID: %d"%fit.ID continue finally: if callback: wx.CallAfter(callback, i) return doc.toprettyxml()