diff --git a/gui/esiFittings.py b/gui/esiFittings.py index 20fc2ed74..9d14ad8fe 100644 --- a/gui/esiFittings.py +++ b/gui/esiFittings.py @@ -131,7 +131,7 @@ class EveFittings(wx.Frame): return data = self.fitTree.fittingsTreeCtrl.GetItemData(selection) sPort = Port.getInstance() - fits = sPort.importFitFromBuffer(data) + import_type, fits = sPort.importFitFromBuffer(data) self.mainFrame._openAfterImport(fits) def deleteFitting(self, event): diff --git a/gui/fitCommands/__init__.py b/gui/fitCommands/__init__.py index 145201559..3501e8089 100644 --- a/gui/fitCommands/__init__.py +++ b/gui/fitCommands/__init__.py @@ -32,4 +32,5 @@ from .guiChangeDroneQty import GuiChangeDroneQty from .guiChangeProjectedDroneQty import GuiChangeProjectedDroneQty from .guiToggleDrone import GuiToggleDroneCommand from .guiFitRename import GuiFitRenameCommand -from .guiChangeImplantLocation import GuiChangeImplantLocation \ No newline at end of file +from .guiChangeImplantLocation import GuiChangeImplantLocation +from .guiImportMutatedModule import GuiImportMutatedModuleCommand diff --git a/gui/fitCommands/calc/fitImportMutatedModule.py b/gui/fitCommands/calc/fitImportMutatedModule.py new file mode 100644 index 000000000..3a28c68ea --- /dev/null +++ b/gui/fitCommands/calc/fitImportMutatedModule.py @@ -0,0 +1,93 @@ +import wx +from eos.saveddata.module import Module, State +import eos.db +from eos.db.gamedata.queries import getDynamicItem +from logbook import Logger +from service.fit import Fit +pyfalog = Logger(__name__) + + +class FitImportMutatedCommand(wx.Command): + """" + Fitting command that takes info about mutated module, composes it and adds it to a fit + """ + def __init__(self, fitID, baseItem, mutaItem, attrMap): + wx.Command.__init__(self, True) + self.fitID = fitID + self.baseItem = baseItem + self.mutaItem = mutaItem + self.attrMap = attrMap + self.new_position = None + self.change = None + self.replace_cmd = None + + def Do(self): + sFit = Fit.getInstance() + fitID = self.fitID + fit = eos.db.getFit(fitID) + + if self.baseItem is None: + pyfalog.warning("Unable to build non-mutated module: no base item to build from") + return False + + try: + mutaTypeID = self.mutaItem.ID + except AttributeError: + mutaplasmid = None + else: + mutaplasmid = getDynamicItem(mutaTypeID) + # Try to build simple item even though no mutaplasmid found + if mutaplasmid is None: + try: + module = Module(self.baseItem) + except ValueError: + pyfalog.warning("Unable to build non-mutated module: {}", self.baseItem) + return False + # Build mutated module otherwise + else: + try: + module = Module(mutaplasmid.resultingItem, self.baseItem, mutaplasmid) + except ValueError: + pyfalog.warning("Unable to build mutated module: {} {}", self.baseItem, self.mutaItem) + return False + else: + for attrID, mutator in module.mutators.items(): + if attrID in self.attrMap: + mutator.value = self.attrMap[attrID] + + + # this is essentially the same as the FitAddModule command. possibly look into centralizing this functionality somewhere? + if module.fits(fit): + pyfalog.debug("Adding {} as module for fit {}", module, fit) + module.owner = fit + numSlots = len(fit.modules) + fit.modules.append(module) + if module.isValidState(State.ACTIVE): + module.state = State.ACTIVE + + # todo: fix these + # As some items may affect state-limiting attributes of the ship, calculate new attributes first + # self.recalc(fit) + # Then, check states of all modules and change where needed. This will recalc if needed + sFit.checkStates(fit, module) + + # fit.fill() + eos.db.commit() + + self.change = numSlots != len(fit.modules) + self.new_position = module.modPosition + else: + return False + + return True + + def Undo(self): + # We added a subsystem module, which actually ran the replace command. Run the undo for that guy instead + if self.replace_cmd: + return self.replace_cmd.Undo() + + from .fitRemoveModule import FitRemoveModuleCommand # Avoid circular import + if self.new_position is not None: + cmd = FitRemoveModuleCommand(self.fitID, [self.new_position]) + cmd.Do() + return True diff --git a/gui/fitCommands/guiImportMutatedModule.py b/gui/fitCommands/guiImportMutatedModule.py new file mode 100644 index 000000000..9187adf7e --- /dev/null +++ b/gui/fitCommands/guiImportMutatedModule.py @@ -0,0 +1,38 @@ +import wx +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from .calc.fitImportMutatedModule import FitImportMutatedCommand +from service.fit import Fit +from logbook import Logger +pyfalog = Logger(__name__) + + +class GuiImportMutatedModuleCommand(wx.Command): + + def __init__(self, fitID, baseItem, mutaItem, attrMap): + wx.Command.__init__(self, True, "Mutated Module Import: {} {} {}".format(baseItem, mutaItem, attrMap)) + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.sFit = Fit.getInstance() + self.fitID = fitID + self.baseItem = baseItem + self.mutaItem = mutaItem + self.attrMap = attrMap + self.internal_history = wx.CommandProcessor() + + def Do(self): + pyfalog.debug("{} Do()".format(self)) + + if self.internal_history.Submit(FitImportMutatedCommand(self.fitID, self.baseItem, self.mutaItem, self.attrMap)): + self.sFit.recalc(self.fitID) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID, action="modadd")) + return True + return False + + def Undo(self): + pyfalog.debug("{} Undo()".format(self)) + for _ in self.internal_history.Commands: + self.internal_history.Undo() + self.sFit.recalc(self.fitID) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID, action="moddel")) + return True diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 2a3665f74..5f3aa6441 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -73,6 +73,7 @@ from service.fit import Fit from service.port import EfsPort, IPortUser, Port from service.settings import HTMLExportSettings, SettingsProvider from service.update import Update +import gui.fitCommands as cmd disableOverrideEditor = False @@ -728,12 +729,18 @@ class MainFrame(wx.Frame): def importFromClipboard(self, event): clipboard = fromClipboard() + activeFit = self.getActiveFit() try: - fits = Port().importFitFromBuffer(clipboard, self.getActiveFit()) + importType, importData = Port().importFitFromBuffer(clipboard, activeFit) + # If it's mutated item - make sure there's at least base item specified + if importType == "MutatedItem": + # we've imported an Abyssal module, need to fire off the command to add it to the fit + self.command.Submit(cmd.GuiImportMutatedModuleCommand(activeFit, *importData[0])) + return # no need to do anything else except: pyfalog.error("Attempt to import failed:\n{0}", clipboard) else: - self._openAfterImport(fits) + self._openAfterImport(importData) def exportToClipboard(self, event): CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft, diff --git a/service/port/eft.py b/service/port/eft.py index fc8576d01..a1e99f45c 100644 --- a/service/port/eft.py +++ b/service/port/eft.py @@ -22,7 +22,7 @@ import re from logbook import Logger -from eos.db.gamedata.queries import getAttributeInfo, getDynamicItem +from eos.db.gamedata.queries import getDynamicItem from eos.saveddata.cargo import Cargo from eos.saveddata.citadel import Citadel from eos.saveddata.booster import Booster @@ -32,10 +32,10 @@ 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 service.port.muta import parseMutant, renderMutant +from service.port.shared import IPortUser, fetchItem, processing_notify from enum import Enum @@ -157,17 +157,7 @@ def exportEft(fit, options): 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)) + mutationLines.append(renderMutant(mutant, firstPrefix='[{}] '.format(mutantReference), prefix=' ')) if mutationLines: sections.append('\n'.join(mutationLines)) @@ -507,59 +497,48 @@ def _importPrepareString(eftString): return lines +mutantHeaderPattern = re.compile('^\[(?P\d+)\](?P.*)') + + def _importGetMutationData(lines): data = {} + # Format: {ref: [lines]} + mutaLinesMap = {} + currentMutaRef = None + currentMutaLines = [] consumedIndices = set() - for i in range(len(lines)): - line = lines[i] - m = re.match('^\[(?P\d+)\]', line) + + def completeMutaLines(): + if currentMutaRef is not None and currentMutaLines: + mutaLinesMap[currentMutaRef] = currentMutaLines + + for i, line in enumerate(lines): + m = mutantHeaderPattern.match(line) + # Start and reset at header 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 + completeMutaLines() + currentMutaRef = int(m.group('ref')) + currentMutaLines = [] + currentMutaLines.append(m.group('tail')) + consumedIndices.add(i) + # Reset at blank line + elif not line: + completeMutaLines() + currentMutaRef = None + currentMutaLines = [] + elif currentMutaRef is not None: + currentMutaLines.append(line) + consumedIndices.add(i) + else: + completeMutaLines() + # Clear mutant info from source for i in sorted(consumedIndices, reverse=True): del lines[i] + # Run parsing + data = {} + for ref, mutaLines in mutaLinesMap.items(): + _, mutaType, mutaAttrs = parseMutant(mutaLines) + data[ref] = (mutaType, mutaAttrs) return data @@ -587,7 +566,7 @@ def _importCreateFit(lines): shipType = m.group('shipType').strip() fitName = m.group('fitName').strip() try: - ship = _fetchItem(shipType) + ship = fetchItem(shipType) try: fit.ship = Ship(ship) except ValueError: @@ -599,20 +578,6 @@ def _importCreateFit(lines): 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] @@ -667,7 +632,7 @@ class Section: class BaseItemSpec: def __init__(self, typeName): - item = _fetchItem(typeName, eagerCat=True) + item = fetchItem(typeName, eagerCat=True) if item is None: raise EftImportError self.typeName = typeName @@ -704,7 +669,7 @@ class RegularItemSpec(BaseItemSpec): def __fetchCharge(self, chargeName): if chargeName: - charge = _fetchItem(chargeName, eagerCat=True) + charge = fetchItem(chargeName, eagerCat=True) if not charge or charge.category.name != 'Charge': charge = None else: diff --git a/service/port/muta.py b/service/port/muta.py new file mode 100644 index 000000000..dc1b4492c --- /dev/null +++ b/service/port/muta.py @@ -0,0 +1,79 @@ +# ============================================================================= +# 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 eos.db.gamedata.queries import getAttributeInfo +from gui.utils.numberFormatter import roundToPrec +from service.port.shared import fetchItem + + +def renderMutant(mutant, firstPrefix='', prefix=''): + exportLines = [] + mutatedAttrs = {} + for attrID, mutator in mutant.mutators.items(): + attrName = getAttributeInfo(attrID).name + mutatedAttrs[attrName] = mutator.value + exportLines.append('{}{}'.format(firstPrefix, mutant.baseItem.name)) + exportLines.append('{}{}'.format(prefix, mutant.mutaplasmid.item.name)) + # Round to 7th significant number to avoid exporting float errors + customAttrsLine = ', '.join( + '{} {}'.format(a, roundToPrec(mutatedAttrs[a], 7)) + for a in sorted(mutatedAttrs)) + exportLines.append('{}{}'.format(prefix, customAttrsLine)) + return '\n'.join(exportLines) + + +def parseMutant(lines): + # Fetch base item type + try: + baseName = lines[0] + except IndexError: + return None + baseType = fetchItem(baseName.strip()) + if baseType is None: + return None, None, {} + # Fetch mutaplasmid item type and actual item + try: + mutaName = lines[1] + except IndexError: + return baseType, None, {} + mutaType = fetchItem(mutaName.strip()) + if mutaType is None: + return baseType, None, {} + # Process mutated attribute values + try: + mutaAttrsLine = lines[2] + except IndexError: + return baseType, mutaType, {} + mutaAttrs = {} + 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 + return baseType, mutaType, mutaAttrs diff --git a/service/port/port.py b/service/port/port.py index 560af14ea..cad3dfae2 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -37,6 +37,7 @@ 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 +from service.port.muta import parseMutant pyfalog = Logger(__name__) @@ -188,18 +189,20 @@ class Port(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 + importType, importData = Port.importAuto(bufferStr, activeFit=activeFit) + + if importType != "MutatedItem": + for fit in importData: + 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 importType, importData @classmethod def importAuto(cls, string, path=None, activeFit=None, iportuser=None): @@ -228,8 +231,16 @@ class Port(object): if re.match("\[.*,.*\]", firstLine): return "EFT", (cls.importEft(string),) - # Use DNA format for all other cases - return "DNA", (cls.importDna(string),) + # Check if string is in DNA format + if re.match("\d+(:\d+(;\d+))*::", firstLine): + return "DNA", (cls.importDna(string),) + + # Assume that we import stand-alone abyssal module if all else fails + try: + return "MutatedItem", (parseMutant(string.split("\n")),) + except: + pass + # EFT-related methods @staticmethod diff --git a/service/port/shared.py b/service/port/shared.py index a21a81d63..214a7f3fe 100644 --- a/service/port/shared.py +++ b/service/port/shared.py @@ -20,6 +20,13 @@ from abc import ABCMeta, abstractmethod +from logbook import Logger + +from service.market import Market + + +pyfalog = Logger(__name__) + class UserCancelException(Exception): """when user cancel on port processing.""" @@ -68,3 +75,17 @@ class IPortUser(metaclass=ABCMeta): def processing_notify(iportuser, flag, data): if not iportuser.on_port_processing(flag, data): raise UserCancelException + + +def fetchItem(typeName, eagerCat=False): + sMkt = Market.getInstance() + eager = 'group.category' if eagerCat else None + try: + item = sMkt.getItem(typeName, eager=eager) + except: + pyfalog.warning('service.port.shared: unable to fetch item "{}"'.format(typeName)) + return None + if sMkt.getPublicityByItem(item): + return item + else: + return None