From 82d8c95d2fac205e1d054f2d1af0f53307818496 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 23 Aug 2018 19:22:24 +0300 Subject: [PATCH 01/29] Rework export format for abyssal mods, now uses references --- service/port.py | 54 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/service/port.py b/service/port.py index 5ad5c4ed3..87ed7ac46 100644 --- a/service/port.py +++ b/service/port.py @@ -1066,7 +1066,7 @@ class Port(object): @classmethod - def exportEft(cls, fit, mutations=False, implants=False): + def exportEft(cls, fit, mutations=True, 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 @@ -1075,37 +1075,33 @@ class Port(object): 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 = {} + # Format: {reference number: module} + mutants = {} + mutantReference = 1 sFit = svcFit.getInstance() for module in fit.modules: slot = module.slot slotTypeMods = modsBySlotType.setdefault(slot, []) if module.item: - mutatedMod = bool(module.mutators) + mutated = bool(module.mutators) # if module was mutated, use base item name for export - if mutatedMod: + if mutated: modName = module.baseItem.name else: modName = module.item.name + if mutated and mutations: + mutants[mutantReference] = module + mutationSuffix = ' [{}]'.format(mutantReference) + mutantReference += 1 + else: + mutationSuffix = '' modOfflineSuffix = offineSuffix if module.state == State.OFFLINE else '' if module.charge and sFit.serviceFittingOptions['exportCharges']: - slotTypeMods.append('{}, {}{}'.format(modName, module.charge.name, modOfflineSuffix)) + slotTypeMods.append('{}, {}{}{}'.format(modName, module.charge.name, modOfflineSuffix, mutationSuffix)) 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)) + slotTypeMods.append('{}{}{}'.format(modName, modOfflineSuffix, mutationSuffix)) else: slotTypeMods.append('[Empty {} slot]'.format(Slot.getName(slot).capitalize() if slot is not None else '')) modSection = [] @@ -1157,6 +1153,28 @@ class Port(object): if cargoLines: sections.append('\n'.join(cargoLines)) + # Section 5: mutated modules' details + mutationLines = [] + if mutants and mutations: + + def formatAttrVal(val): + if int(val) == val: + return int(val) + return val + + 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)) + customAttrsLine = ', '.join('{} {}'.format(a, formatAttrVal(mutatedAttrs[a])) 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)) @classmethod From 6240f23c02fd957b48bbdacb1b6cca7dc39abeb5 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 24 Aug 2018 09:54:03 +0300 Subject: [PATCH 02/29] Enumerate mutated modules based on their export order rather than how pyfa order, and round attribute values to avoid float errors --- service/port.py | 65 +++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/service/port.py b/service/port.py index 87ed7ac46..d964f7bb5 100644 --- a/service/port.py +++ b/service/port.py @@ -33,6 +33,7 @@ import xml.parsers.expat from eos import db from eos.db.gamedata.queries import getAttributeInfo +from gui.utils.numberFormatter import roundToPrec from service.fit import Fit as svcFit # noinspection PyPackageRequirements @@ -1075,41 +1076,40 @@ class Port(object): header = '[{}, {}]'.format(fit.ship.item.name, fit.name) # Section 1: modules, rigs, subsystems, services - offineSuffix = ' /OFFLINE' modsBySlotType = {} - # Format: {reference number: module} - mutants = {} - mutantReference = 1 sFit = svcFit.getInstance() for module in fit.modules: slot = module.slot - slotTypeMods = modsBySlotType.setdefault(slot, []) - 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 mutations: - mutants[mutantReference] = module - mutationSuffix = ' [{}]'.format(mutantReference) - mutantReference += 1 - else: - mutationSuffix = '' - modOfflineSuffix = offineSuffix if module.state == State.OFFLINE else '' - if module.charge and sFit.serviceFittingOptions['exportCharges']: - slotTypeMods.append('{}, {}{}{}'.format(modName, module.charge.name, modOfflineSuffix, mutationSuffix)) - else: - slotTypeMods.append('{}{}{}'.format(modName, modOfflineSuffix, mutationSuffix)) - else: - slotTypeMods.append('[Empty {} slot]'.format(Slot.getName(slot).capitalize() if slot is not None else '')) + modsBySlotType.setdefault(slot, []).append(module) + # Format: {reference number: module} modSection = [] + offineSuffix = ' /OFFLINE' + mutants = {} + mutantReference = 1 for slotType in EFT_SLOT_ORDER: rackLines = [] - data = modsBySlotType.get(slotType, ()) - for line in data: - rackLines.append(line) + 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 mutations: + mutants[mutantReference] = module + mutationSuffix = ' [{}]'.format(mutantReference) + mutantReference += 1 + else: + mutationSuffix = '' + modOfflineSuffix = offineSuffix 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: @@ -1156,12 +1156,6 @@ class Port(object): # Section 5: mutated modules' details mutationLines = [] if mutants and mutations: - - def formatAttrVal(val): - if int(val) == val: - return int(val) - return val - for mutantReference in sorted(mutants): mutant = mutants[mutantReference] mutatedAttrs = {} @@ -1170,7 +1164,8 @@ class Port(object): mutatedAttrs[attrName] = mutator.value mutationLines.append('[{}] {}'.format(mutantReference, mutant.baseItem.name)) mutationLines.append(' {}'.format(mutant.mutaplasmid.item.name)) - customAttrsLine = ', '.join('{} {}'.format(a, formatAttrVal(mutatedAttrs[a])) for a in sorted(mutatedAttrs)) + # 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)) From 0a2fa62e21e37c4e3d8c9133d18572a5848a7884 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 24 Aug 2018 09:55:32 +0300 Subject: [PATCH 03/29] Fix misplaced comment --- service/port.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service/port.py b/service/port.py index d964f7bb5..13795c65e 100644 --- a/service/port.py +++ b/service/port.py @@ -1081,10 +1081,9 @@ class Port(object): for module in fit.modules: slot = module.slot modsBySlotType.setdefault(slot, []).append(module) - # Format: {reference number: module} modSection = [] offineSuffix = ' /OFFLINE' - mutants = {} + mutants = {} # Format: {reference number: module} mutantReference = 1 for slotType in EFT_SLOT_ORDER: rackLines = [] From 83222489c4b039108671f93f7bd8060051e96278 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 24 Aug 2018 10:01:31 +0300 Subject: [PATCH 04/29] Condense code a bit more --- service/port.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service/port.py b/service/port.py index 13795c65e..f5fec6378 100644 --- a/service/port.py +++ b/service/port.py @@ -1079,8 +1079,7 @@ class Port(object): modsBySlotType = {} sFit = svcFit.getInstance() for module in fit.modules: - slot = module.slot - modsBySlotType.setdefault(slot, []).append(module) + modsBySlotType.setdefault(module.slot, []).append(module) modSection = [] offineSuffix = ' /OFFLINE' mutants = {} # Format: {reference number: module} From 66d559306fbdd7776bc207b359b85ddff6863172 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 24 Aug 2018 10:03:44 +0300 Subject: [PATCH 05/29] Avoid doing too wide lines --- service/port.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/service/port.py b/service/port.py index f5fec6378..8a6cf7343 100644 --- a/service/port.py +++ b/service/port.py @@ -1103,11 +1103,13 @@ class Port(object): mutationSuffix = '' modOfflineSuffix = offineSuffix if module.state == State.OFFLINE else '' if module.charge and sFit.serviceFittingOptions['exportCharges']: - rackLines.append('{}, {}{}{}'.format(modName, module.charge.name, modOfflineSuffix, mutationSuffix)) + 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 '')) + rackLines.append('[Empty {} slot]'.format( + Slot.getName(slotType).capitalize() if slotType is not None else '')) if rackLines: modSection.append('\n'.join(rackLines)) if modSection: @@ -1146,7 +1148,10 @@ class Port(object): # 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)): + 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)) @@ -1163,7 +1168,9 @@ class Port(object): 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)) + customAttrsLine = ', '.join( + '{} {}'.format(a, roundToPrec(mutatedAttrs[a], 7)) + for a in sorted(mutatedAttrs)) mutationLines.append(' {}'.format(customAttrsLine)) if mutationLines: sections.append('\n'.join(mutationLines)) From 4a27c60486c5b6355e8e99054d295ab7bd496432 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 24 Aug 2018 13:25:41 +0300 Subject: [PATCH 06/29] Clean up the fit being imported before parsing --- service/port.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/service/port.py b/service/port.py index 8a6cf7343..a0f8ddf32 100644 --- a/service/port.py +++ b/service/port.py @@ -625,21 +625,28 @@ class Port(object): @staticmethod def importEft(eftString): + # Split passed string in lines and clean them up + 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] + sMkt = Market.getInstance() - offineSuffix = " /OFFLINE" + 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 + # Ship and fit name from header + header = lines.pop(0) + m = re.match('\[(?P[\w\s]+), (?P.+)\]', header) + if not m: + pyfalog.warning('Corrupted fit header in importEft') + return + shipType = m.group('shipType').strip() + fitName = m.group('fitName').strip() try: ship = sMkt.getItem(shipType) try: From 2dd1ddddd59266f9d812793f1be6e3dccda801e1 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 24 Aug 2018 18:57:42 +0300 Subject: [PATCH 07/29] Further rework of import function --- service/port.py | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/service/port.py b/service/port.py index a0f8ddf32..f67c6e4ff 100644 --- a/service/port.py +++ b/service/port.py @@ -658,6 +658,91 @@ class Port(object): pyfalog.warning("Exception caught in importEft") return + def sectionIter(lines): + section = [] + for line in lines: + if not line: + if section: + yield section + section = [] + section.append(line) + if section: + yield section + + stubPattern = '^\[.+\]$' + modulePattern = '^(?P[^,/]+)(, (?P[^,/]+))?(?P /OFFLINE)?( \[(?P\d+)\])?$' + droneCargoPattern = '^(?P[^,/]+) x(?P\d+)$' + + sections = [] + for section in sectionIter(lines): + sectionItemData = [] + for line in section: + # Stub line + if re.match(stubPattern, line): + sectionItemData.append({'type': 'stub'}) + continue + # Items with quantity specifier + m = re.match(droneCargoPattern, line) + if m: + sectionItemData.append({ + 'type': 'multi', + 'typeName': m.group('typeName'), + 'amount': int(m.group('amount'))}) + continue + # All other items + m = re.match(modulePattern, line) + if m: + sectionItemData.append({ + 'type': 'normal', + 'typeName': m.group('typeName'), + 'chargeName': m.group('charge'), + 'offline': True if m.group('offline') else False, + 'mutation': int(m.group('mutation')) if m.group('mutation') else None}) + # Strip stubs from tail + while sectionItemData and sectionItemData[-1]['type'] == 'stub': + del sectionItemData[-1] + if sectionItemData: + sections.append(sectionItemData) + + + + for sectionItemData in sections: + sectionCats = set() + for entry in sectionItemData: + if entry['type'] == 'stub': + continue + try: + item = sMkt.getItem(entry['typeName'], eager='group.category') + except: + pyfalog.warning('no data can be found (old names)') + entry['type'] = 'stub' + continue + entry['item'] = item + import sys + sys.stderr.write('{}\n'.format(item.slot)) + sectionCats.add(item.category.name) + processStubs = ( + # To process stubs, we must make sure that all the items in section + # are from the same category + len(sectionCats) == 1 and tuple(sectionCats)[0] == 'Module' and + # And that they do not contain items with quantity specifier + all(i['type'] in ('stub', 'normal') for i in sectionItemData)) + isDronebay = ( + len(sectionCats) == 1 and tuple(sectionCats)[0] == 'Drone' and + all(i['type'] == 'multi' for i in sectionItemData)) + isFighterbay = ( + len(sectionCats) == 1 and tuple(sectionCats)[0] == 'Fighter' and + all(i['type'] == 'multi' for i in sectionItemData)) + + + + + + + + + + # maintain map of drones and their quantities droneMap = {} cargoMap = {} From a8c69abc72ae817d28f4213be4ceeb2d70aad2d0 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 25 Aug 2018 11:06:33 +0300 Subject: [PATCH 08/29] Move EFT import/export related functions to separate file --- service/eftPort.py | 526 +++++++++++++++++++++++++++++++++++++++++++++ service/port.py | 371 +------------------------------- 2 files changed, 534 insertions(+), 363 deletions(-) create mode 100644 service/eftPort.py diff --git a/service/eftPort.py b/service/eftPort.py new file mode 100644 index 000000000..de69bd966 --- /dev/null +++ b/service/eftPort.py @@ -0,0 +1,526 @@ +# ============================================================================= +# 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 +from eos.saveddata.citadel import Citadel +from eos.saveddata.module import Module, Slot, State +from eos.saveddata.ship import Ship +from gui.utils.numberFormatter import roundToPrec +from service.fit import Fit as svcFit +from service.market import Market + + +pyfalog = Logger(__name__) + + +def fetchItem(typeName, eagerCat=True): + sMkt = Market.getInstance() + eager = 'group.category' if eagerCat else None + try: + return sMkt.getItem(typeName, eager=eager) + except: + pyfalog.warning('EftPort: unable to fetch item "{}"'.format(typeName)) + return + + +class EftImportError(Exception): + """Exception class emitted and consumed by EFT importer/exporter internally.""" + ... + + +class AmountMap(dict): + + def add(self, entity, amount): + if entity not in self: + self[entity] = 0 + self[entity] += amount + + +class AbstractFit: + + def __init__(self): + self.modulesHigh = [] + self.modulesMed = [] + self.modulesLow = [] + self.rigs = [] + self.subsystems = [] + self.services = [] + self.drones = AmountMap() + self.fighters = AmountMap() + self.implants = set() + self.boosters = set() + self.cargo = AmountMap() + + # def addModule(self, m): + # modContMap = { + # Slot.HIGH: self.modulesHigh, + # Slot.MED: self.modulesMed, + # Slot.LOW: self.modulesLow, + # Slot.RIG: self.rigs, + # Slot.SUBSYSTEM: self.subsystems, + # Slot.SERVICE: self.services} + + +class Section: + + def __init__(self): + self.lines = [] + self.itemData = [] + self.__itemDataCats = None + + @property + def itemDataCats(self): + if self.__itemDataCats is None: + cats = set() + for itemSpec in self.itemData: + if itemSpec is None: + continue + cats.add(itemSpec.item.category.name) + self.__itemDataCats = tuple(sorted(cats)) + return self.__itemDataCats + + def cleanItemDataTail(self): + while self.itemData and self.itemData[-1] is None: + del self.itemData[-1] + + +class BaseItemSpec: + + def __init__(self, typeName): + item = fetchItem(typeName, eagerCat=True) + if item is None: + raise EftImportError + self.typeName = typeName + self.item = item + + +class ItemSpec(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 charge.category.name != 'Charge': + charge = None + else: + charge = None + return charge + + +class MultiItemSpec(BaseItemSpec): + + def __init__(self, typeName): + super().__init__(typeName) + self.amount = 0 + + +class EftPort: + + SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE] + OFFLINE_SUFFIX = ' /OFFLINE' + + @classmethod + def exportEft(cls, fit, mutations, implants): + # 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 cls.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 mutations: + mutants[mutantReference] = module + mutationSuffix = ' [{}]'.format(mutantReference) + mutantReference += 1 + else: + mutationSuffix = '' + modOfflineSuffix = cls.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 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)) + + # Section 5: mutated modules' details + mutationLines = [] + if mutants and mutations: + 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)) + + @classmethod + def importEft(cls, eftString): + lines = cls.__prepareImportString(eftString) + try: + fit = cls.__createFit(lines) + except EftImportError: + return + + aFit = AbstractFit() + + stubPattern = '^\[.+\]$' + modulePattern = '^(?P[^,/]+)(, (?P[^,/]+))?(?P{})?( \[(?P\d+)\])?$'.format(cls.OFFLINE_SUFFIX) + droneCargoPattern = '^(?P[^,/]+) x(?P\d+)$' + + dronebaySeen = False + fightersSeen = False + for section in cls.__importSectionIter(lines): + for line in section.lines: + # Stub line + if re.match(stubPattern, line): + section.itemData.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.itemData.append(None) + else: + itemSpec.amount = int(m.group('amount')) + section.itemData.append(itemSpec) + # All other items + m = re.match(modulePattern, line) + if m: + try: + itemSpec = ItemSpec(m.group('typeName'), chargeName=m.group('chargeName')) + # Items which cannot be fetched are considered as stubs + except EftImportError: + section.itemData.append(None) + else: + if m.group('offline'): + itemSpec.offline = True + if m.group('mutation'): + itemSpec.mutationIdx = int(m.group('mutation')) + section.itemData.append(itemSpec) + section.cleanItemDataTail() + # Finally, start putting items into intermediate containers + # All items in section have quantity specifier + if all(isinstance(id, MultiItemSpec) for id in section.itemData): + # Dronebay + if len(section.itemDataCats) == 1 and section.itemDataCats[0] == 'Drone' and not dronebaySeen: + for entry in section.itemData: + aFit.drones.add(entry['typeName'], entry['amount']) + dronebaySeen = True + # Fighters + elif len(section.itemDataCats) == 1 and section.itemDataCats[0] == 'Fighter' and not fightersSeen: + for entry in section.itemData: + aFit.fighters.add(entry['typeName'], entry['amount']) + fightersSeen = True + # Cargo + else: + for entry in section.itemData: + aFit.cargo.add(entry['typeName'], entry['amount']) + # All of items are normal or stubs + elif all(isinstance(id, ItemSpec) or id is None for id in section.itemData): + if len(section.itemDataCats) == 1: + if section.itemDataCats[0] in ('Module', 'Subsystem', 'Structure Module'): + slotTypes = set() + for entry in itemData: + if entry['type'] == 'stub': + continue + try: + m = Module(entry['item']) + except ValueError: + m = None + else: + slotTypes.add(m.slot) + entry['module'] = m + # If whole section uses container of the same type, + if len(slotTypes) == 1: + pass + else: + pass + + else: + pass + else: + pass + + # Mix between all types + else: + pass + + + + # 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 __prepareImportString(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 + + @classmethod + def __createFit(cls, lines): + """Create fit and set top-level entity (ship or citadel).""" + fit = Fit() + header = lines.pop(0) + m = re.match('\[(?P[\w\s]+), (?P.+)\]', header) + if not m: + pyfalog.warning('EftPort.importEft: corrupted fit header') + raise EftImportError + shipType = m.group('shipType').strip() + fitName = m.group('fitName').strip() + try: + ship = fetchItem(shipType, eagerCat=False) + try: + fit.ship = Ship(ship) + except ValueError: + fit.ship = Citadel(ship) + fit.name = fitName + except: + pyfalog.warning('EftPort.importEft: exception caught when parsing header') + raise EftImportError + return fit + + @staticmethod + 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 diff --git a/service/port.py b/service/port.py index f67c6e4ff..bf1ff4f90 100644 --- a/service/port.py +++ b/service/port.py @@ -32,8 +32,6 @@ from codecs import open import xml.parsers.expat from eos import db -from eos.db.gamedata.queries import getAttributeInfo -from gui.utils.numberFormatter import roundToPrec from service.fit import Fit as svcFit # noinspection PyPackageRequirements @@ -53,6 +51,7 @@ from utils.strfunctions import sequential_rep, replace_ltgt from abc import ABCMeta, abstractmethod from service.esi import Esi +from service.eftPort import EftPort from collections import OrderedDict @@ -62,7 +61,6 @@ class ESIExportException(Exception): 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, @@ -213,11 +211,7 @@ class IPortUser(metaclass=ABCMeta): 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 - """ + """Service which houses all import/export format functions""" instance = None __tag_replace_flag = True @@ -351,8 +345,6 @@ class Port(object): db.save(fit) return fits - """Service which houses all import/export format functions""" - @classmethod def exportESI(cls, ofit, callback=None): # A few notes: @@ -625,248 +617,7 @@ class Port(object): @staticmethod def importEft(eftString): - # Split passed string in lines and clean them up - 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] - - sMkt = Market.getInstance() - offineSuffix = ' /OFFLINE' - - fit = Fit() - - # Ship and fit name from header - header = lines.pop(0) - m = re.match('\[(?P[\w\s]+), (?P.+)\]', header) - if not m: - pyfalog.warning('Corrupted fit header in importEft') - return - shipType = m.group('shipType').strip() - fitName = m.group('fitName').strip() - 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 - - def sectionIter(lines): - section = [] - for line in lines: - if not line: - if section: - yield section - section = [] - section.append(line) - if section: - yield section - - stubPattern = '^\[.+\]$' - modulePattern = '^(?P[^,/]+)(, (?P[^,/]+))?(?P /OFFLINE)?( \[(?P\d+)\])?$' - droneCargoPattern = '^(?P[^,/]+) x(?P\d+)$' - - sections = [] - for section in sectionIter(lines): - sectionItemData = [] - for line in section: - # Stub line - if re.match(stubPattern, line): - sectionItemData.append({'type': 'stub'}) - continue - # Items with quantity specifier - m = re.match(droneCargoPattern, line) - if m: - sectionItemData.append({ - 'type': 'multi', - 'typeName': m.group('typeName'), - 'amount': int(m.group('amount'))}) - continue - # All other items - m = re.match(modulePattern, line) - if m: - sectionItemData.append({ - 'type': 'normal', - 'typeName': m.group('typeName'), - 'chargeName': m.group('charge'), - 'offline': True if m.group('offline') else False, - 'mutation': int(m.group('mutation')) if m.group('mutation') else None}) - # Strip stubs from tail - while sectionItemData and sectionItemData[-1]['type'] == 'stub': - del sectionItemData[-1] - if sectionItemData: - sections.append(sectionItemData) - - - - for sectionItemData in sections: - sectionCats = set() - for entry in sectionItemData: - if entry['type'] == 'stub': - continue - try: - item = sMkt.getItem(entry['typeName'], eager='group.category') - except: - pyfalog.warning('no data can be found (old names)') - entry['type'] = 'stub' - continue - entry['item'] = item - import sys - sys.stderr.write('{}\n'.format(item.slot)) - sectionCats.add(item.category.name) - processStubs = ( - # To process stubs, we must make sure that all the items in section - # are from the same category - len(sectionCats) == 1 and tuple(sectionCats)[0] == 'Module' and - # And that they do not contain items with quantity specifier - all(i['type'] in ('stub', 'normal') for i in sectionItemData)) - isDronebay = ( - len(sectionCats) == 1 and tuple(sectionCats)[0] == 'Drone' and - all(i['type'] == 'multi' for i in sectionItemData)) - isFighterbay = ( - len(sectionCats) == 1 and tuple(sectionCats)[0] == 'Fighter' and - all(i['type'] == 'multi' for i in sectionItemData)) - - - - - - - - - - - # 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 + return EftPort.importEft(eftString) @staticmethod def importEftCfg(shipname, contents, iportuser=None): @@ -1159,119 +910,12 @@ class Port(object): @classmethod - def exportEft(cls, fit, mutations=True, 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 - modsBySlotType = {} - sFit = svcFit.getInstance() - for module in fit.modules: - modsBySlotType.setdefault(module.slot, []).append(module) - modSection = [] - offineSuffix = ' /OFFLINE' - mutants = {} # Format: {reference number: module} - mutantReference = 1 - for slotType in EFT_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 mutations: - mutants[mutantReference] = module - mutationSuffix = ' [{}]'.format(mutantReference) - mutantReference += 1 - else: - mutationSuffix = '' - modOfflineSuffix = offineSuffix 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 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)) - - # Section 5: mutated modules' details - mutationLines = [] - if mutants and mutations: - 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 exportEft(cls, fit): + return EftPort.exportEft(fit, mutations=False, implants=False) @classmethod def exportEftImps(cls, fit): - return cls.exportEft(fit, implants=True) + return EftPort.exportEft(fit, mutations=False, implants=True) @staticmethod def exportDna(fit): @@ -1478,7 +1122,8 @@ class Port(object): class PortProcessing(object): - """Port Processing class """ + """Port Processing class""" + @staticmethod def backupFits(path, iportuser): success = True From eb8fa2f259b36f73409d733a02368142bae2d9f1 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 25 Aug 2018 16:03:45 +0300 Subject: [PATCH 09/29] Rework EFT importing --- service/eftPort.py | 496 +++++++++++++++++++++++++-------------------- 1 file changed, 272 insertions(+), 224 deletions(-) diff --git a/service/eftPort.py b/service/eftPort.py index de69bd966..028e7e274 100644 --- a/service/eftPort.py +++ b/service/eftPort.py @@ -23,9 +23,15 @@ import re from logbook import Logger from eos.db.gamedata.queries import getAttributeInfo +from eos.saveddata.cargo import Cargo from eos.saveddata.citadel import Citadel -from eos.saveddata.module import Module, Slot, State +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 @@ -33,76 +39,74 @@ from service.market import Market pyfalog = Logger(__name__) +MODULE_CATS = ('Module', 'Subsystem', 'Structure Module') +SLOT_ORDER = (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE) +OFFLINE_SUFFIX = ' /OFFLINE' + def fetchItem(typeName, eagerCat=True): sMkt = Market.getInstance() eager = 'group.category' if eagerCat else None try: - return sMkt.getItem(typeName, eager=eager) + item = sMkt.getItem(typeName, eager=eager) except: pyfalog.warning('EftPort: unable to fetch item "{}"'.format(typeName)) - return + 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/exporter internally.""" + """Exception class emitted and consumed by EFT importer internally.""" ... -class AmountMap(dict): - - def add(self, entity, amount): - if entity not in self: - self[entity] = 0 - self[entity] += amount - - -class AbstractFit: - - def __init__(self): - self.modulesHigh = [] - self.modulesMed = [] - self.modulesLow = [] - self.rigs = [] - self.subsystems = [] - self.services = [] - self.drones = AmountMap() - self.fighters = AmountMap() - self.implants = set() - self.boosters = set() - self.cargo = AmountMap() - - # def addModule(self, m): - # modContMap = { - # Slot.HIGH: self.modulesHigh, - # Slot.MED: self.modulesMed, - # Slot.LOW: self.modulesLow, - # Slot.RIG: self.rigs, - # Slot.SUBSYSTEM: self.subsystems, - # Slot.SERVICE: self.services} - - class Section: def __init__(self): self.lines = [] - self.itemData = [] + self.itemSpecs = [] self.__itemDataCats = None @property def itemDataCats(self): if self.__itemDataCats is None: cats = set() - for itemSpec in self.itemData: + for itemSpec in self.itemSpecs: if itemSpec is None: continue cats.add(itemSpec.item.category.name) self.__itemDataCats = tuple(sorted(cats)) return self.__itemDataCats - def cleanItemDataTail(self): - while self.itemData and self.itemData[-1] is None: - del self.itemData[-1] + @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: @@ -114,8 +118,28 @@ class BaseItemSpec: self.typeName = typeName self.item = item + @property + def isModule(self): + return False -class ItemSpec(BaseItemSpec): + @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) @@ -132,6 +156,17 @@ class ItemSpec(BaseItemSpec): 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): @@ -139,12 +174,127 @@ class MultiItemSpec(BaseItemSpec): 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} + + @property + def modContMap(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 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) + modContMap = self.modContMap + # If all the modules have same slot type, put them to appropriate + # container with stubs + if len(slotTypes) == 1: + slotType = tuple(slotTypes)[0] + modContMap[slotType].extend(modules) + # Otherwise, put just modules + else: + for m in modules: + if m is None: + continue + modContMap[m.slot].append(m) + + def addModule(self, itemSpec): + if itemSpec is None: + return + m = self.__makeModule(itemSpec) + if m is not None: + self.modContMap[m.slot].append(m) + + def __makeModule(self, itemSpec): + 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 + class EftPort: - SLOT_ORDER = [Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE] - OFFLINE_SUFFIX = ' /OFFLINE' - @classmethod def exportEft(cls, fit, mutations, implants): # EFT formatted export is split in several sections, each section is @@ -163,7 +313,7 @@ class EftPort: mutants = {} # Format: {reference number: module} mutantReference = 1 - for slotType in cls.SLOT_ORDER: + for slotType in SLOT_ORDER: rackLines = [] modules = modsBySlotType.get(slotType, ()) for module in modules: @@ -180,7 +330,7 @@ class EftPort: mutantReference += 1 else: mutationSuffix = '' - modOfflineSuffix = cls.OFFLINE_SUFFIX if module.state == State.OFFLINE else '' + modOfflineSuffix = OFFLINE_SUFFIX if module.state == State.OFFLINE else '' if module.charge and sFit.serviceFittingOptions['exportCharges']: rackLines.append('{}, {}{}{}'.format( modName, module.charge.name, modOfflineSuffix, mutationSuffix)) @@ -267,16 +417,15 @@ class EftPort: aFit = AbstractFit() stubPattern = '^\[.+\]$' - modulePattern = '^(?P[^,/]+)(, (?P[^,/]+))?(?P{})?( \[(?P\d+)\])?$'.format(cls.OFFLINE_SUFFIX) + modulePattern = '^(?P[^,/]+)(, (?P[^,/]+))?(?P{})?( \[(?P\d+)\])?$'.format(OFFLINE_SUFFIX) droneCargoPattern = '^(?P[^,/]+) x(?P\d+)$' - dronebaySeen = False - fightersSeen = False + sections = [] for section in cls.__importSectionIter(lines): for line in section.lines: # Stub line if re.match(stubPattern, line): - section.itemData.append(None) + section.itemSpecs.append(None) continue # Items with quantity specifier m = re.match(droneCargoPattern, line) @@ -285,197 +434,96 @@ class EftPort: itemSpec = MultiItemSpec(m.group('typeName')) # Items which cannot be fetched are considered as stubs except EftImportError: - section.itemData.append(None) + section.itemSpecs.append(None) else: itemSpec.amount = int(m.group('amount')) - section.itemData.append(itemSpec) + section.itemSpecs.append(itemSpec) # All other items m = re.match(modulePattern, line) if m: try: - itemSpec = ItemSpec(m.group('typeName'), chargeName=m.group('chargeName')) + itemSpec = RegularItemSpec(m.group('typeName'), chargeName=m.group('chargeName')) # Items which cannot be fetched are considered as stubs except EftImportError: - section.itemData.append(None) + section.itemSpecs.append(None) else: if m.group('offline'): itemSpec.offline = True if m.group('mutation'): itemSpec.mutationIdx = int(m.group('mutation')) - section.itemData.append(itemSpec) - section.cleanItemDataTail() - # Finally, start putting items into intermediate containers - # All items in section have quantity specifier - if all(isinstance(id, MultiItemSpec) for id in section.itemData): - # Dronebay - if len(section.itemDataCats) == 1 and section.itemDataCats[0] == 'Drone' and not dronebaySeen: - for entry in section.itemData: - aFit.drones.add(entry['typeName'], entry['amount']) - dronebaySeen = True - # Fighters - elif len(section.itemDataCats) == 1 and section.itemDataCats[0] == 'Fighter' and not fightersSeen: - for entry in section.itemData: - aFit.fighters.add(entry['typeName'], entry['amount']) - fightersSeen = True - # Cargo - else: - for entry in section.itemData: - aFit.cargo.add(entry['typeName'], entry['amount']) - # All of items are normal or stubs - elif all(isinstance(id, ItemSpec) or id is None for id in section.itemData): - if len(section.itemDataCats) == 1: - if section.itemDataCats[0] in ('Module', 'Subsystem', 'Structure Module'): - slotTypes = set() - for entry in itemData: - if entry['type'] == 'stub': - continue - try: - m = Module(entry['item']) - except ValueError: - m = None - else: - slotTypes.add(m.slot) - entry['module'] = m - # If whole section uses container of the same type, - if len(slotTypes) == 1: - pass - else: - pass + section.itemSpecs.append(itemSpec) + clearTail(section.itemSpecs) + sections.append(section) - else: - pass - else: - pass - - # Mix between all types + 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: - pass + 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) - - - # 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: + # Subsystems first because they modify slot amount + for subsystem in aFit.subsystems: + if subsystem is None: 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 + if subsystem.fits(fit): + subsystem.owner = fit + fit.modules.append(subsystem) 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) - + # Other stuff + for modRack in ( + aFit.rigs, + aFit.services, + aFit.modulesHigh, + aFit.modulesMed, + aFit.modulesLow, + ): + for m in modRack: + if m is None: + continue + if m.fits(fit): + m.owner = fit + if not m.isValidState(m.state): + pyfalog.warning('EftPort.importEft: module {} cannot have state {}', m, m.state) + fit.modules.append(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 @staticmethod From 44aed364b70daab42217dce9331a829a91d5c72c Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 25 Aug 2018 16:21:28 +0300 Subject: [PATCH 10/29] Fix oversight which broke dronebay/fighterbay/cargo detection --- service/eftPort.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/service/eftPort.py b/service/eftPort.py index 028e7e274..7c3df50e3 100644 --- a/service/eftPort.py +++ b/service/eftPort.py @@ -438,6 +438,7 @@ class EftPort: else: itemSpec.amount = int(m.group('amount')) section.itemSpecs.append(itemSpec) + continue # All other items m = re.match(modulePattern, line) if m: @@ -452,6 +453,7 @@ class EftPort: if m.group('mutation'): itemSpec.mutationIdx = int(m.group('mutation')) section.itemSpecs.append(itemSpec) + continue clearTail(section.itemSpecs) sections.append(section) @@ -524,6 +526,7 @@ class EftPort: fit.fighters.append(fighter) for cargo in aFit.cargo.values(): fit.cargo.append(cargo) + return fit @staticmethod From 93f1a18b37aeafcebf1c2a423dcd2c4feb7fd251 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 25 Aug 2018 17:42:19 +0300 Subject: [PATCH 11/29] Respect spaces between the modules during import --- eos/effectHandlerHelpers.py | 4 ++++ service/eftPort.py | 43 +++++++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index f890ae0ed..927958702 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -113,6 +113,7 @@ class HandledList(list): class HandledModuleList(HandledList): + def append(self, mod): emptyPosition = float("Inf") for i in range(len(self)): @@ -130,6 +131,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/service/eftPort.py b/service/eftPort.py index 7c3df50e3..ca7cc1be6 100644 --- a/service/eftPort.py +++ b/service/eftPort.py @@ -205,7 +205,7 @@ class AbstractFit: self.cargo = {} # Format: {item: Cargo} @property - def modContMap(self): + def __slotContainerMap(self): return { Slot.HIGH: self.modulesHigh, Slot.MED: self.modulesMed, @@ -214,6 +214,17 @@ class AbstractFit: 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() @@ -228,25 +239,24 @@ class AbstractFit: modules.append(m) slotTypes.add(m.slot) clearTail(modules) - modContMap = self.modContMap # If all the modules have same slot type, put them to appropriate # container with stubs if len(slotTypes) == 1: slotType = tuple(slotTypes)[0] - modContMap[slotType].extend(modules) + self.getContainerBySlot(slotType).extend(modules) # Otherwise, put just modules else: for m in modules: if m is None: continue - modContMap[m.slot].append(m) + 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.modContMap[m.slot].append(m) + self.getContainerBySlot(m.slot).append(m) def __makeModule(self, itemSpec): try: @@ -492,12 +502,15 @@ class EftPort: aFit.addCargo(itemSpec) # Subsystems first because they modify slot amount - for subsystem in aFit.subsystems: - if subsystem is None: - continue - if subsystem.fits(fit): - subsystem.owner = fit - fit.modules.append(subsystem) + 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 + pyfalog.error('kurwa {}'.format(type(fit.modules))) + fit.modules.appendIgnoreEmpty(m) svcFit.getInstance().recalc(fit) # Other stuff @@ -510,12 +523,14 @@ class EftPort: ): for m in modRack: if m is None: - continue - if m.fits(fit): + 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('EftPort.importEft: module {} cannot have state {}', m, m.state) - fit.modules.append(m) + fit.modules.appendIgnoreEmpty(m) for implant in aFit.implants: fit.implants.append(implant) for booster in aFit.boosters: From f2c2e2e65a7aebc2f1c66fba3a166a31a296e8bd Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 25 Aug 2018 19:27:03 +0300 Subject: [PATCH 12/29] Implement mutated attributes import --- eos/db/gamedata/queries.py | 15 ++++ service/eftPort.py | 142 +++++++++++++++++++++++++++++-------- service/port.py | 1 - 3 files changed, 126 insertions(+), 32 deletions(-) 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/service/eftPort.py b/service/eftPort.py index ca7cc1be6..c5b1da145 100644 --- a/service/eftPort.py +++ b/service/eftPort.py @@ -22,7 +22,7 @@ import re from logbook import Logger -from eos.db.gamedata.queries import getAttributeInfo +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 @@ -44,7 +44,7 @@ SLOT_ORDER = (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERV OFFLINE_SUFFIX = ' /OFFLINE' -def fetchItem(typeName, eagerCat=True): +def fetchItem(typeName, eagerCat=False): sMkt = Market.getInstance() eager = 'group.category' if eagerCat else None try: @@ -150,7 +150,7 @@ class RegularItemSpec(BaseItemSpec): def __fetchCharge(self, chargeName): if chargeName: charge = fetchItem(chargeName, eagerCat=True) - if charge.category.name != 'Charge': + if not charge or charge.category.name != 'Charge': charge = None else: charge = None @@ -203,6 +203,8 @@ class AbstractFit: 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): @@ -259,10 +261,28 @@ class AbstractFit: self.getContainerBySlot(m.slot).append(m) def __makeModule(self, itemSpec): - try: - m = Module(itemSpec.item) - except ValueError: - return None + # 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): @@ -425,10 +445,13 @@ class EftPort: return aFit = AbstractFit() + aFit.mutations = cls.__getMutationData(lines) + pyfalog.error('{}'.format(aFit.mutations)) + nameChars = '[^,/\[\]]' # Characters which are allowed to be used in name stubPattern = '^\[.+\]$' - modulePattern = '^(?P[^,/]+)(, (?P[^,/]+))?(?P{})?( \[(?P\d+)\])?$'.format(OFFLINE_SUFFIX) - droneCargoPattern = '^(?P[^,/]+) x(?P\d+)$' + modulePattern = '^(?P{0}+)(, (?P{0}+))?(?P{1})?( \[(?P\d+)\])?$'.format(nameChars, OFFLINE_SUFFIX) + droneCargoPattern = '^(?P{}+) x(?P\d+)$'.format(nameChars) sections = [] for section in cls.__importSectionIter(lines): @@ -467,6 +490,7 @@ class EftPort: 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: @@ -555,28 +579,61 @@ class EftPort: del lines[-1] return lines - @classmethod - def __createFit(cls, lines): - """Create fit and set top-level entity (ship or citadel).""" - fit = Fit() - header = lines.pop(0) - m = re.match('\[(?P[\w\s]+), (?P.+)\]', header) - if not m: - pyfalog.warning('EftPort.importEft: corrupted fit header') - raise EftImportError - shipType = m.group('shipType').strip() - fitName = m.group('fitName').strip() - try: - ship = fetchItem(shipType, eagerCat=False) - try: - fit.ship = Ship(ship) - except ValueError: - fit.ship = Citadel(ship) - fit.name = fitName - except: - pyfalog.warning('EftPort.importEft: exception caught when parsing header') - raise EftImportError - return fit + @staticmethod + def __getMutationData(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 @staticmethod def __importSectionIter(lines): @@ -590,3 +647,26 @@ class EftPort: section.lines.append(line) if section.lines: yield section + + @classmethod + def __createFit(cls, lines): + """Create fit and set top-level entity (ship or citadel).""" + fit = Fit() + header = lines.pop(0) + m = re.match('\[(?P[\w\s]+), (?P.+)\]', header) + if not m: + pyfalog.warning('EftPort.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('EftPort.importEft: exception caught when parsing header') + raise EftImportError + return fit diff --git a/service/port.py b/service/port.py index bf1ff4f90..1337825c8 100644 --- a/service/port.py +++ b/service/port.py @@ -908,7 +908,6 @@ class Port(object): return fit_list - @classmethod def exportEft(cls, fit): return EftPort.exportEft(fit, mutations=False, implants=False) From 18ee37d8fd8af16a23c7ad2a99f2c3001217771d Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 25 Aug 2018 19:30:48 +0300 Subject: [PATCH 13/29] Fix small oversight --- service/port.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/port.py b/service/port.py index 1337825c8..810d9b919 100644 --- a/service/port.py +++ b/service/port.py @@ -51,7 +51,7 @@ from utils.strfunctions import sequential_rep, replace_ltgt from abc import ABCMeta, abstractmethod from service.esi import Esi -from service.eftPort import EftPort +from service.eftPort import EftPort, SLOT_ORDER as EFT_SLOT_ORDER from collections import OrderedDict From ebe0efac8185b51efdb91b237691e4482a2568d7 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 25 Aug 2018 17:24:08 -0400 Subject: [PATCH 14/29] First bit of work for GUI support for abyssal export. --- gui/copySelectDialog.py | 74 +++++++++++++++++++++++++++++------------ gui/mainFrame.py | 22 ++++++------ service/eftPort.py | 27 ++++++++++++--- service/port.py | 4 +-- 4 files changed, 88 insertions(+), 39 deletions(-) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 9f5291026..9fd2c7e21 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -20,41 +20,59 @@ # noinspection PyPackageRequirements import wx +from service.eftPort import EFT_OPTIONS 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.copyFormats = { + "EFT": CopySelectDialog.copyFormatEft, + "XML": CopySelectDialog.copyFormatXml, + "DNA": CopySelectDialog.copyFormatDna, + "ESI": CopySelectDialog.copyFormatEsi, + "MultiBuy": CopySelectDialog.copyFormatMultiBuy, + "EFS": CopySelectDialog.copyFormatEfs + } + + 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) + mainSizer.Add(rdo, 0, wx.EXPAND | wx.ALL, 5) self.copyFormat = CopySelectDialog.copyFormatEft - selector.SetSelection(self.copyFormat) - mainSizer.Add(selector, 0, wx.EXPAND | wx.ALL, 5) + # some sizer magic to deal with https://github.com/wxWidgets/Phoenix/issues/974 + self.box1 = wx.StaticBox(self, -1, "EFT Options") + self.bsizer1 = wx.BoxSizer(wx.VERTICAL) + self.bsizer2 = wx.BoxSizer(wx.VERTICAL) + self.bsizer1.AddSpacer(10) + self.bsizer1.Add(self.bsizer2, 1, wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) + + self.options = {} + + for x, v in EFT_OPTIONS.items(): + ch = wx.CheckBox(self.box1, -1, v['name']) + self.options[x] = ch + self.bsizer2.Add(ch, 1, wx.EXPAND) + + self.box1.SetSizer(self.bsizer1) + + mainSizer.Add(self.box1, 0, wx.EXPAND | wx.ALL, 5) buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL) if buttonSizer: mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, 5) @@ -64,7 +82,19 @@ class CopySelectDialog(wx.Dialog): self.Center() def Selected(self, event): - self.copyFormat = event.GetSelection() + obj = event.GetEventObject() + format = obj.GetLabel() + self.box1.Show(format == "EFT") + self.Fit() + self.copyFormat = self.copyFormats[format] def GetSelected(self): return self.copyFormat + + def GetOptions(self): + i = 0 + for x, v in self.options.items(): + if v.IsChecked(): + i = i ^ x.value + return i + diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 9bd02feee..ba6715908 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -703,31 +703,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)) @@ -742,7 +742,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, @@ -751,8 +751,8 @@ class MainFrame(wx.Frame): dlg = CopySelectDialog(self) dlg.ShowModal() selected = dlg.GetSelected() - - CopySelectDict[selected]() + options = dlg.GetOptions() + CopySelectDict[selected](options) try: dlg.Destroy() diff --git a/service/eftPort.py b/service/eftPort.py index c5b1da145..775c89cfa 100644 --- a/service/eftPort.py +++ b/service/eftPort.py @@ -35,14 +35,33 @@ 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 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: { + "name": "Implants", + "description": "Exports implants" + }, + Options.MUTATIONS: { + "name": "Abyssal", + "description": "Exports Abyssal stats" + } + # 4: [] +} + def fetchItem(typeName, eagerCat=False): sMkt = Market.getInstance() @@ -326,7 +345,7 @@ class AbstractFit: class EftPort: @classmethod - def exportEft(cls, fit, mutations, implants): + def exportEft(cls, 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 @@ -354,7 +373,7 @@ class EftPort: modName = module.baseItem.name else: modName = module.item.name - if mutated and mutations: + if mutated and options & Options.MUTATIONS.value: mutants[mutantReference] = module mutationSuffix = ' [{}]'.format(mutantReference) mutantReference += 1 @@ -390,7 +409,7 @@ class EftPort: sections.append('\n\n'.join(minionSection)) # Section 3: implants, boosters - if implants: + if options & Options.IMPLANTS.value: charSection = [] implantLines = [] for implant in fit.implants: @@ -417,7 +436,7 @@ class EftPort: # Section 5: mutated modules' details mutationLines = [] - if mutants and mutations: + if mutants and options & Options.MUTATIONS.value: for mutantReference in sorted(mutants): mutant = mutants[mutantReference] mutatedAttrs = {} diff --git a/service/port.py b/service/port.py index 810d9b919..7e7ec7aea 100644 --- a/service/port.py +++ b/service/port.py @@ -909,8 +909,8 @@ class Port(object): return fit_list @classmethod - def exportEft(cls, fit): - return EftPort.exportEft(fit, mutations=False, implants=False) + def exportEft(cls, fit, options): + return EftPort.exportEft(fit, options) @classmethod def exportEftImps(cls, fit): From c530660132e8f57d6c89910b1d909fa4ce08ec38 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 25 Aug 2018 19:01:49 -0400 Subject: [PATCH 15/29] Save last export setting --- gui/copySelectDialog.py | 14 +++++++++++--- gui/mainFrame.py | 5 +++++ service/eftPort.py | 4 ++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 9fd2c7e21..d130b3174 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -21,6 +21,7 @@ # noinspection PyPackageRequirements import wx from service.eftPort import EFT_OPTIONS +from service.settings import SettingsProvider class CopySelectDialog(wx.Dialog): @@ -36,6 +37,8 @@ class CopySelectDialog(wx.Dialog): style=wx.DEFAULT_DIALOG_STYLE) mainSizer = wx.BoxSizer(wx.VERTICAL) + self.settings = SettingsProvider.getInstance().getSettings("pyfaExport", {"format": 0, "options": 0}) + self.copyFormats = { "EFT": CopySelectDialog.copyFormatEft, "XML": CopySelectDialog.copyFormatXml, @@ -51,10 +54,11 @@ class CopySelectDialog(wx.Dialog): 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) - self.copyFormat = CopySelectDialog.copyFormatEft - # some sizer magic to deal with https://github.com/wxWidgets/Phoenix/issues/974 self.box1 = wx.StaticBox(self, -1, "EFT Options") self.bsizer1 = wx.BoxSizer(wx.VERTICAL) @@ -68,6 +72,8 @@ class CopySelectDialog(wx.Dialog): for x, v in EFT_OPTIONS.items(): ch = wx.CheckBox(self.box1, -1, v['name']) self.options[x] = ch + if self.settings['options'] & x: + ch.SetValue(True) self.bsizer2.Add(ch, 1, wx.EXPAND) self.box1.SetSizer(self.bsizer1) @@ -77,6 +83,8 @@ class CopySelectDialog(wx.Dialog): if buttonSizer: mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, 5) + self.box1.Show(self.GetSelected() == CopySelectDialog.copyFormatEft) + self.SetSizer(mainSizer) self.Fit() self.Center() @@ -95,6 +103,6 @@ class CopySelectDialog(wx.Dialog): i = 0 for x, v in self.options.items(): if v.IsChecked(): - i = i ^ x.value + i = i ^ x return i diff --git a/gui/mainFrame.py b/gui/mainFrame.py index ba6715908..c89914e44 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -752,6 +752,11 @@ class MainFrame(wx.Frame): dlg.ShowModal() selected = dlg.GetSelected() options = dlg.GetOptions() + + settings = SettingsProvider.getInstance().getSettings("pyfaExport") + settings["format"] = selected + settings["options"] = options + CopySelectDict[selected](options) try: diff --git a/service/eftPort.py b/service/eftPort.py index 775c89cfa..dfab8312d 100644 --- a/service/eftPort.py +++ b/service/eftPort.py @@ -51,11 +51,11 @@ SLOT_ORDER = (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERV OFFLINE_SUFFIX = ' /OFFLINE' EFT_OPTIONS = { - Options.IMPLANTS: { + Options.IMPLANTS.value: { "name": "Implants", "description": "Exports implants" }, - Options.MUTATIONS: { + Options.MUTATIONS.value: { "name": "Abyssal", "description": "Exports Abyssal stats" } From 3faa57f39aa2bedf80bf3df6754face752381853 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sun, 26 Aug 2018 11:10:14 +0300 Subject: [PATCH 16/29] Remove debugging log requests --- service/eftPort.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/service/eftPort.py b/service/eftPort.py index dfab8312d..01adfc6ec 100644 --- a/service/eftPort.py +++ b/service/eftPort.py @@ -465,7 +465,6 @@ class EftPort: aFit = AbstractFit() aFit.mutations = cls.__getMutationData(lines) - pyfalog.error('{}'.format(aFit.mutations)) nameChars = '[^,/\[\]]' # Characters which are allowed to be used in name stubPattern = '^\[.+\]$' @@ -552,7 +551,6 @@ class EftPort: fit.modules.appendIgnoreEmpty(dummy) elif m.fits(fit): m.owner = fit - pyfalog.error('kurwa {}'.format(type(fit.modules))) fit.modules.appendIgnoreEmpty(m) svcFit.getInstance().recalc(fit) From 0a47fba10767f381b48edd504d24e6cc542ee1ae Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 28 Aug 2018 19:00:39 +0300 Subject: [PATCH 17/29] Move import/export facilities to their own folder --- gui/copySelectDialog.py | 2 +- gui/esiFittings.py | 2 +- gui/mainFrame.py | 3 +-- service/port/__init__.py | 2 ++ service/{efsPort.py => port/efs.py} | 0 service/{eftPort.py => port/eft.py} | 0 service/{ => port}/port.py | 2 +- 7 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 service/port/__init__.py rename service/{efsPort.py => port/efs.py} (100%) rename service/{eftPort.py => port/eft.py} (100%) rename service/{ => port}/port.py (99%) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index d130b3174..11fa79bb9 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -20,7 +20,7 @@ # noinspection PyPackageRequirements import wx -from service.eftPort import EFT_OPTIONS +from service.port.eft import EFT_OPTIONS from service.settings import SettingsProvider diff --git a/gui/esiFittings.py b/gui/esiFittings.py index af3da77a5..5b7dec36d 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.port import ESIExportException pyfalog = Logger(__name__) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index c89914e44..ced466577 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 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/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/eftPort.py b/service/port/eft.py similarity index 100% rename from service/eftPort.py rename to service/port/eft.py diff --git a/service/port.py b/service/port/port.py similarity index 99% rename from service/port.py rename to service/port/port.py index 7e7ec7aea..5b942263e 100644 --- a/service/port.py +++ b/service/port/port.py @@ -51,7 +51,7 @@ from utils.strfunctions import sequential_rep, replace_ltgt from abc import ABCMeta, abstractmethod from service.esi import Esi -from service.eftPort import EftPort, SLOT_ORDER as EFT_SLOT_ORDER +from service.port.eft import EftPort, SLOT_ORDER as EFT_SLOT_ORDER from collections import OrderedDict From d1e6647d1f7c5c4efb5c1adb469d2aede7505c64 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Thu, 30 Aug 2018 00:07:37 -0400 Subject: [PATCH 18/29] Don't show/hide options box, move options to under EFT, and simply disable them when EFT not selected --- gui/copySelectDialog.py | 40 ++++++++++++++++++---------------------- service/port/eft.py | 2 +- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 11fa79bb9..45191f6d9 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -48,6 +48,8 @@ class CopySelectDialog(wx.Dialog): "EFS": CopySelectDialog.copyFormatEfs } + 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) @@ -59,32 +61,22 @@ class CopySelectDialog(wx.Dialog): self.copyFormat = self.copyFormats[format] mainSizer.Add(rdo, 0, wx.EXPAND | wx.ALL, 5) - # some sizer magic to deal with https://github.com/wxWidgets/Phoenix/issues/974 - self.box1 = wx.StaticBox(self, -1, "EFT Options") - self.bsizer1 = wx.BoxSizer(wx.VERTICAL) - self.bsizer2 = wx.BoxSizer(wx.VERTICAL) - self.bsizer1.AddSpacer(10) + if format == "EFT": + bsizer = wx.BoxSizer(wx.VERTICAL) - self.bsizer1.Add(self.bsizer2, 1, wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10) + 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) - self.options = {} - - for x, v in EFT_OPTIONS.items(): - ch = wx.CheckBox(self.box1, -1, v['name']) - self.options[x] = ch - if self.settings['options'] & x: - ch.SetValue(True) - self.bsizer2.Add(ch, 1, wx.EXPAND) - - self.box1.SetSizer(self.bsizer1) - - mainSizer.Add(self.box1, 0, wx.EXPAND | wx.ALL, 5) buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL) if buttonSizer: mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, 5) - self.box1.Show(self.GetSelected() == CopySelectDialog.copyFormatEft) - + self.toggleOptions() self.SetSizer(mainSizer) self.Fit() self.Center() @@ -92,9 +84,13 @@ class CopySelectDialog(wx.Dialog): def Selected(self, event): obj = event.GetEventObject() format = obj.GetLabel() - self.box1.Show(format == "EFT") - self.Fit() 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 diff --git a/service/port/eft.py b/service/port/eft.py index 01adfc6ec..ad984a068 100644 --- a/service/port/eft.py +++ b/service/port/eft.py @@ -56,7 +56,7 @@ EFT_OPTIONS = { "description": "Exports implants" }, Options.MUTATIONS.value: { - "name": "Abyssal", + "name": "Mutated Attributes", "description": "Exports Abyssal stats" } # 4: [] From ba157af496604976c99e3ee106167f6f6f84c96f Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 14:25:40 +0300 Subject: [PATCH 19/29] Allow format to have no space between: ship name - fit name; module name - charge name; module spec - offline suffix; module spec - mutastats reference This fixes import of modules with charges from zkb and few other issues --- service/port/eft.py | 12 ++++++------ service/port/port.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/service/port/eft.py b/service/port/eft.py index ad984a068..fac4b8508 100644 --- a/service/port/eft.py +++ b/service/port/eft.py @@ -48,7 +48,7 @@ class Options(Enum): MODULE_CATS = ('Module', 'Subsystem', 'Structure Module') SLOT_ORDER = (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE) -OFFLINE_SUFFIX = ' /OFFLINE' +OFFLINE_SUFFIX = '/OFFLINE' EFT_OPTIONS = { Options.IMPLANTS.value: { @@ -379,7 +379,7 @@ class EftPort: mutantReference += 1 else: mutationSuffix = '' - modOfflineSuffix = OFFLINE_SUFFIX if module.state == State.OFFLINE else '' + 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)) @@ -467,9 +467,9 @@ class EftPort: aFit.mutations = cls.__getMutationData(lines) nameChars = '[^,/\[\]]' # Characters which are allowed to be used in name - stubPattern = '^\[.+\]$' - modulePattern = '^(?P{0}+)(, (?P{0}+))?(?P{1})?( \[(?P\d+)\])?$'.format(nameChars, OFFLINE_SUFFIX) - droneCargoPattern = '^(?P{}+) x(?P\d+)$'.format(nameChars) + 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 cls.__importSectionIter(lines): @@ -670,7 +670,7 @@ class EftPort: """Create fit and set top-level entity (ship or citadel).""" fit = Fit() header = lines.pop(0) - m = re.match('\[(?P[\w\s]+), (?P.+)\]', header) + m = re.match('\[(?P[\w\s]+),\s*(?P.+)\]', header) if not m: pyfalog.warning('EftPort.importEft: corrupted fit header') raise EftImportError diff --git a/service/port/port.py b/service/port/port.py index 5b942263e..9f359b36b 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -814,7 +814,7 @@ class Port(object): @staticmethod def importXml(text, iportuser=None): - # type: (basestring, IPortUser, basestring) -> list[eos.saveddata.fit.Fit] + # type: (str, IPortUser) -> list[eos.saveddata.fit.Fit] sMkt = Market.getInstance() doc = xml.dom.minidom.parseString(text) # NOTE: From 302cab54fdae03e58b2f742f10584eb0a130bc48 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 15:23:24 +0300 Subject: [PATCH 20/29] Get rid of PortProcessing object --- service/port/eft.py | 890 +++++++++++++++++++++++++---------------- service/port/port.py | 271 ++----------- service/port/shared.py | 69 ++++ 3 files changed, 643 insertions(+), 587 deletions(-) create mode 100644 service/port/shared.py diff --git a/service/port/eft.py b/service/port/eft.py index fac4b8508..80640bfff 100644 --- a/service/port/eft.py +++ b/service/port/eft.py @@ -35,6 +35,7 @@ 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 @@ -59,10 +60,550 @@ EFT_OPTIONS = { "name": "Mutated Attributes", "description": "Exports Abyssal stats" } - # 4: [] } +class EftPort: + + @classmethod + def exportEft(cls, 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)) + + @classmethod + def importEft(cls, eftString): + lines = cls.__prepareImportString(eftString) + try: + fit = cls.__createFit(lines) + except EftImportError: + return + + aFit = AbstractFit() + aFit.mutations = cls.__getMutationData(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 cls.__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('EftPort.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 + + @staticmethod + 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 + + @staticmethod + def __prepareImportString(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 + + @staticmethod + def __getMutationData(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 + + @staticmethod + 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 + + @staticmethod + def __createFit(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('EftPort.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('EftPort.importEft: exception caught when parsing header') + raise EftImportError + return fit + +# Various methods and functions which assist with EFT import-export + def fetchItem(typeName, eagerCat=False): sMkt = Market.getInstance() eager = 'group.category' if eagerCat else None @@ -340,350 +881,3 @@ class AbstractFit: if itemSpec.item not in self.cargo: self.cargo[itemSpec.item] = Cargo(itemSpec.item) self.cargo[itemSpec.item].amount += itemSpec.amount - - -class EftPort: - - @classmethod - def exportEft(cls, 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)) - - @classmethod - def importEft(cls, eftString): - lines = cls.__prepareImportString(eftString) - try: - fit = cls.__createFit(lines) - except EftImportError: - return - - aFit = AbstractFit() - aFit.mutations = cls.__getMutationData(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 cls.__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('EftPort.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 - - @staticmethod - def __prepareImportString(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 - - @staticmethod - def __getMutationData(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 - - @staticmethod - 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 - - @classmethod - def __createFit(cls, 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('EftPort.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('EftPort.importEft: exception caught when parsing header') - raise EftImportError - return fit diff --git a/service/port/port.py b/service/port/port.py index 9f359b36b..3fc08aa25 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -38,8 +38,6 @@ from service.fit import Fit as svcFit 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 @@ -52,6 +50,7 @@ from abc import ABCMeta, abstractmethod from service.esi import Esi from service.port.eft import EftPort, SLOT_ORDER as EFT_SLOT_ORDER +from service.port.shared import processing_notify from collections import OrderedDict @@ -235,10 +234,26 @@ class Port(object): @staticmethod def backupFits(path, iportuser): pyfalog.debug("Starting backup fits thread.") -# thread = FitBackupThread(path, callback) -# thread.start() + + 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=PortProcessing.backupFits, + target=backupFitsWorkerFunc, args=(path, iportuser) ).start() @@ -251,10 +266,14 @@ class Port(object): :rtype: None """ pyfalog.debug("Starting import fits thread.") -# thread = FitImportThread(paths, iportuser) -# thread.start() + + 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=PortProcessing.importFitsFromFile, + target=importFitsFromFileWorkerFunc, args=(paths, iportuser) ).start() @@ -275,7 +294,7 @@ class Port(object): if iportuser: # Pulse msg = "Processing file:\n%s" % path pyfalog.debug(msg) - PortProcessing.notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg) + processing_notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg) # wx.CallAfter(callback, 1, msg) with open(path, "rb") as file_: @@ -310,7 +329,7 @@ class Port(object): # IDs.append(fit.ID) if iportuser: # Pulse pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", idx + 1, numFits) - PortProcessing.notify( + 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) ) @@ -621,196 +640,7 @@ class Port(object): @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 + return EftPort.importEftCfg(shipname, contents, iportuser) @staticmethod def importXml(text, iportuser=None): @@ -901,7 +731,7 @@ class Port(object): fit_list.append(fitobj) if iportuser: # NOTE: Send current processing status - PortProcessing.notify( + processing_notify( iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, "Processing %s\n%s" % (fitobj.ship.name, fitobj.name) ) @@ -912,10 +742,6 @@ class Port(object): def exportEft(cls, fit, options): return EftPort.exportEft(fit, options) - @classmethod - def exportEftImps(cls, fit): - return EftPort.exportEft(fit, mutations=False, implants=True) - @staticmethod def exportDna(fit): dna = str(fit.shipID) @@ -1065,7 +891,7 @@ class Port(object): continue finally: if iportuser: - PortProcessing.notify( + processing_notify( iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE, (i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name)) ) @@ -1118,36 +944,3 @@ class Port(object): 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/shared.py b/service/port/shared.py new file mode 100644 index 000000000..39838a907 --- /dev/null +++ b/service/port/shared.py @@ -0,0 +1,69 @@ +# ============================================================================= +# 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 From 02e35181d587f97bc5693296ccf04134414ea77d Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 15:46:29 +0300 Subject: [PATCH 21/29] Move out DNA format into separate file --- service/port/dna.py | 176 ++++++++++++++++++++++++++++++++ service/port/port.py | 223 +++++------------------------------------ service/port/shared.py | 3 +- 3 files changed, 203 insertions(+), 199 deletions(-) create mode 100644 service/port/dna.py 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/port/port.py b/service/port/port.py index 3fc08aa25..29b5eba81 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -46,12 +46,11 @@ 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 service.port.dna import exportDna, importDna from service.port.eft import EftPort, SLOT_ORDER as EFT_SLOT_ORDER -from service.port.shared import processing_notify -from collections import OrderedDict +from service.port.shared import IPortUser, UserCancelException, processing_notify class ESIExportException(Exception): @@ -165,50 +164,6 @@ def _resolve_module(hardware, sMkt, b_localized): 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): """Service which houses all import/export format functions""" instance = None @@ -272,6 +227,7 @@ class Port(object): 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) @@ -548,100 +504,6 @@ class Port(object): 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): - return EftPort.importEft(eftString) - - @staticmethod - def importEftCfg(shipname, contents, iportuser=None): - return EftPort.importEftCfg(shipname, contents, iportuser) - @staticmethod def importXml(text, iportuser=None): # type: (str, IPortUser) -> list[eos.saveddata.fit.Fit] @@ -738,63 +600,6 @@ class Port(object): return fit_list - @classmethod - def exportEft(cls, fit, options): - return EftPort.exportEft(fit, options) - - @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() @@ -944,3 +749,25 @@ class Port(object): export = export[:-1] return export + + # EFT-related methods + @staticmethod + def importEft(eftString): + return EftPort.importEft(eftString) + + @staticmethod + def importEftCfg(shipname, contents, iportuser=None): + return EftPort.importEftCfg(shipname, contents, iportuser) + + @classmethod + def exportEft(cls, fit, options): + return EftPort.exportEft(fit, options) + + # DNA-related methods + @staticmethod + def importDna(string): + return importDna(string) + + @staticmethod + def exportDna(fit): + return exportDna(fit) diff --git a/service/port/shared.py b/service/port/shared.py index 39838a907..a21a81d63 100644 --- a/service/port/shared.py +++ b/service/port/shared.py @@ -56,8 +56,9 @@ class IPortUser(metaclass=ABCMeta): 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. """ + + """return: True is continue process, False is cancel.""" pass def on_port_process_start(self): From dd0fbfddb94843897247ad2135decf51e18cce31 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 15:57:59 +0300 Subject: [PATCH 22/29] Split ESI stuff into its own file --- gui/esiFittings.py | 2 +- service/port/esi.py | 208 +++++++++++++++++++++++++++++++++++++++++++ service/port/port.py | 180 +++---------------------------------- 3 files changed, 219 insertions(+), 171 deletions(-) create mode 100644 service/port/esi.py diff --git a/gui/esiFittings.py b/gui/esiFittings.py index 5b7dec36d..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.port import ESIExportException +from service.port.esi import ESIExportException pyfalog = Logger(__name__) 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/port.py b/service/port/port.py index 29b5eba81..97acaba98 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -47,30 +47,15 @@ from eos.saveddata.fit import Fit, ImplantLocation from service.market import Market from utils.strfunctions import sequential_rep, replace_ltgt -from service.esi import Esi from service.port.dna import exportDna, importDna from service.port.eft import EftPort, SLOT_ORDER as EFT_SLOT_ORDER +from service.port.esi import importESI, exportESI from service.port.shared import IPortUser, UserCancelException, processing_notify -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 # 2017/04/05 NOTE: simple validation, for xml file RE_XML_START = r'<\?xml\s+version="1.0"\s*\?>' @@ -320,89 +305,6 @@ class Port(object): db.save(fit) return fits - @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 @@ -433,77 +335,6 @@ class Port(object): # 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 importXml(text, iportuser=None): # type: (str, IPortUser) -> list[eos.saveddata.fit.Fit] @@ -771,3 +602,12 @@ class Port(object): @staticmethod def exportDna(fit): return exportDna(fit) + + # ESI-related methods + @staticmethod + def importESI(string): + importESI(string) + + @staticmethod + def exportESI(fit): + exportESI(fit) From 4e5a70993e661f5ae5693b0868ef11f7d66ffcee Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 16:15:23 +0300 Subject: [PATCH 23/29] Split XML facilities into their own file as well --- service/port/port.py | 331 +++---------------------------------------- service/port/xml.py | 325 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+), 314 deletions(-) create mode 100644 service/port/xml.py diff --git a/service/port/port.py b/service/port/port.py index 97acaba98..045f5561e 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -19,135 +19,29 @@ import re import os -import xml.dom -from logbook import Logger -import collections -import json import threading -from bs4 import UnicodeDammit - - +import xml.dom +import xml.parsers.expat from codecs import open -import xml.parsers.expat +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 - -# noinspection PyPackageRequirements -import wx - -from eos.saveddata.cargo import Cargo -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 service.port.dna import exportDna, importDna from service.port.eft import EftPort, SLOT_ORDER as EFT_SLOT_ORDER from service.port.esi import importESI, exportESI 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*\?>' -# -- 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 Port(object): """Service which houses all import/export format functions""" @@ -288,7 +182,7 @@ class Port(object): @staticmethod def importFitFromBuffer(bufferStr, activeFit=None): - # type: (basestring, object) -> object + # type: (str, object) -> object # TODO: catch the exception? # activeFit is reserved?, bufferStr is unicode? (assume only clipboard string? sFit = svcFit.getInstance() @@ -307,7 +201,7 @@ class Port(object): @classmethod def importAuto(cls, string, path=None, activeFit=None, iportuser=None): - # type: (basestring, basestring, object, IPortUser, basestring) -> object + # 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() @@ -335,206 +229,6 @@ class Port(object): # Use DNA format for all other cases return "DNA", (cls.importDna(string),) - @staticmethod - def importXml(text, iportuser=None): - # 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 - - @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: - processing_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 @@ -611,3 +305,12 @@ class Port(object): @staticmethod def exportESI(fit): exportESI(fit) + + # XML-related methods + @staticmethod + def importXml(text, iportuser=None): + return importXml(text, iportuser) + + @staticmethod + def exportXml(iportuser=None, *fits): + exportXml(iportuser, *fits) diff --git a/service/port/xml.py b/service/port/xml.py new file mode 100644 index 000000000..d5a5f08e0 --- /dev/null +++ b/service/port/xml.py @@ -0,0 +1,325 @@ +# ============================================================================= +# 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): + # 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() From 52dbd8d9ef98bbdcd7c164af305c633165198998 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 16:17:56 +0300 Subject: [PATCH 24/29] Fix few export/import functions --- service/port/port.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/port/port.py b/service/port/port.py index 045f5561e..b821a126f 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -300,11 +300,11 @@ class Port(object): # ESI-related methods @staticmethod def importESI(string): - importESI(string) + return importESI(string) @staticmethod def exportESI(fit): - exportESI(fit) + return exportESI(fit) # XML-related methods @staticmethod @@ -313,4 +313,4 @@ class Port(object): @staticmethod def exportXml(iportuser=None, *fits): - exportXml(iportuser, *fits) + return exportXml(iportuser, *fits) From 8abad416bd5ca4045d94aadbedaaf9852f6e288d Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 16:23:20 +0300 Subject: [PATCH 25/29] Split multibuy into its own file as well --- service/port/multibuy.py | 68 ++++++++++++++++++++++++++++++++++++++++ service/port/port.py | 63 +++++++------------------------------ 2 files changed, 80 insertions(+), 51 deletions(-) create mode 100644 service/port/multibuy.py 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 index b821a126f..bc09e301a 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -17,6 +17,7 @@ # along with pyfa. If not, see . # ============================================================================= + import re import os import threading @@ -31,8 +32,9 @@ 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 EftPort, SLOT_ORDER as EFT_SLOT_ORDER +from service.port.eft import EftPort from service.port.esi import importESI, exportESI +from service.port.multibuy import exportMultiBuy from service.port.shared import IPortUser, UserCancelException, processing_notify from service.port.xml import importXml, exportXml @@ -229,53 +231,7 @@ class Port(object): # Use DNA format for all other cases return "DNA", (cls.importDna(string),) - @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 - - # EFT-related methods + ### EFT-related methods @staticmethod def importEft(eftString): return EftPort.importEft(eftString) @@ -288,7 +244,7 @@ class Port(object): def exportEft(cls, fit, options): return EftPort.exportEft(fit, options) - # DNA-related methods + ### DNA-related methods @staticmethod def importDna(string): return importDna(string) @@ -297,7 +253,7 @@ class Port(object): def exportDna(fit): return exportDna(fit) - # ESI-related methods + ### ESI-related methods @staticmethod def importESI(string): return importESI(string) @@ -306,7 +262,7 @@ class Port(object): def exportESI(fit): return exportESI(fit) - # XML-related methods + ### XML-related methods @staticmethod def importXml(text, iportuser=None): return importXml(text, iportuser) @@ -314,3 +270,8 @@ class Port(object): @staticmethod def exportXml(iportuser=None, *fits): return exportXml(iportuser, *fits) + + ### Multibuy-related methods + @staticmethod + def exportMultiBuy(fit): + return exportMultiBuy(fit) From f4311d1cefddd3933edfaf1acf87605c2caac325 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 21:39:28 +0300 Subject: [PATCH 26/29] Remove object which we don't actually need --- service/port/eft.py | 1017 +++++++++++++++++++++--------------------- service/port/port.py | 10 +- 2 files changed, 511 insertions(+), 516 deletions(-) diff --git a/service/port/eft.py b/service/port/eft.py index 80640bfff..645a2ca2d 100644 --- a/service/port/eft.py +++ b/service/port/eft.py @@ -63,548 +63,543 @@ EFT_OPTIONS = { } -class EftPort: +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 = [] - @classmethod - def exportEft(cls, 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) - 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 = [] - # 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)) + 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: - 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)) - - @classmethod - def importEft(cls, eftString): - lines = cls.__prepareImportString(eftString) - try: - fit = cls.__createFit(lines) - except EftImportError: - return - - aFit = AbstractFit() - aFit.mutations = cls.__getMutationData(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 cls.__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) + 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: - 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) + 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)) - # Subsystems first because they modify slot amount - for m in aFit.subsystems: + # 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(aFit.subsystems)) + 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('EftPort.importEft: module {} cannot have state {}', m, m.state) fit.modules.appendIgnoreEmpty(m) - svcFit.getInstance().recalc(fit) + 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) - # 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('EftPort.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 - return fit - @staticmethod - def importEftCfg(shipname, contents, iportuser): - """Handle import from EFT config store file""" +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] - # 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] - + # 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: - # 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)) + 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: + 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 - # 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) + # Create module + m = Module(modItem) - 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") + # 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: - 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 + 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) - # Create module - m = Module(modItem) + # Recalc to get slot numbers correct for T3 cruisers + svcFit.getInstance().recalc(fitobj) - # 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) + for module in moduleList: + if module.fits(fitobj): + fitobj.modules.append(module) - # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(fitobj) + # Append fit to list of fits + fits.append(fitobj) - for module in moduleList: - if module.fits(fitobj): - fitobj.modules.append(module) + if iportuser: # NOTE: Send current processing status + processing_notify( + iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, + "%s:\n%s" % (fitobj.ship.name, fitobj.name) + ) - # Append fit to list of fits - fits.append(fitobj) + # Skip fit silently if we get an exception + except Exception as e: + pyfalog.error("Caught exception on fit.") + pyfalog.error(e) + pass - if iportuser: # NOTE: Send current processing status - processing_notify( - iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, - "%s:\n%s" % (fitobj.ship.name, fitobj.name) - ) + return fits - # 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 - @staticmethod - def __prepareImportString(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 - - @staticmethod - def __getMutationData(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 - - @staticmethod - 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 - - @staticmethod - def __createFit(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('EftPort.importEft: corrupted fit header') - raise EftImportError - shipType = m.group('shipType').strip() - fitName = m.group('fitName').strip() - try: - ship = fetchItem(shipType) +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: - fit.ship = Ship(ship) - except ValueError: - fit.ship = Citadel(ship) - fit.name = fitName - except: - pyfalog.warning('EftPort.importEft: exception caught when parsing header') - raise EftImportError - return fit + 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 -# Various methods and functions which assist with EFT import-export -def fetchItem(typeName, eagerCat=False): +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('EftPort.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('EftPort.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: @@ -618,7 +613,7 @@ def fetchItem(typeName, eagerCat=False): return None -def clearTail(lst): +def _clearTail(lst): while lst and lst[-1] is None: del lst[-1] @@ -672,7 +667,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 @@ -709,7 +704,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: @@ -800,7 +795,7 @@ class AbstractFit: continue modules.append(m) slotTypes.add(m.slot) - clearTail(modules) + _clearTail(modules) # If all the modules have same slot type, put them to appropriate # container with stubs if len(slotTypes) == 1: diff --git a/service/port/port.py b/service/port/port.py index bc09e301a..021b89e07 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -32,8 +32,8 @@ 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 EftPort -from service.port.esi import importESI, exportESI +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 @@ -234,15 +234,15 @@ class Port(object): ### EFT-related methods @staticmethod def importEft(eftString): - return EftPort.importEft(eftString) + return importEft(eftString) @staticmethod def importEftCfg(shipname, contents, iportuser=None): - return EftPort.importEftCfg(shipname, contents, iportuser) + return importEftCfg(shipname, contents, iportuser) @classmethod def exportEft(cls, fit, options): - return EftPort.exportEft(fit, options) + return exportEft(fit, options) ### DNA-related methods @staticmethod From b05f1573c699e8e6b5e95cc22ad6fb1b199a0685 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 21:41:50 +0300 Subject: [PATCH 27/29] Fix log messages --- service/port/eft.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/service/port/eft.py b/service/port/eft.py index 645a2ca2d..fbcd4c3b2 100644 --- a/service/port/eft.py +++ b/service/port/eft.py @@ -288,7 +288,7 @@ def importEft(eftString): elif m.fits(fit): m.owner = fit if not m.isValidState(m.state): - pyfalog.warning('EftPort.importEft: module {} cannot have state {}', m, 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) @@ -582,7 +582,7 @@ def _importCreateFit(lines): header = lines.pop(0) m = re.match('\[(?P[\w\s]+),\s*(?P.+)\]', header) if not m: - pyfalog.warning('EftPort.importEft: corrupted fit header') + pyfalog.warning('service.port.eft.importEft: corrupted fit header') raise EftImportError shipType = m.group('shipType').strip() fitName = m.group('fitName').strip() @@ -594,7 +594,7 @@ def _importCreateFit(lines): fit.ship = Citadel(ship) fit.name = fitName except: - pyfalog.warning('EftPort.importEft: exception caught when parsing header') + pyfalog.warning('service.port.eft.importEft: exception caught when parsing header') raise EftImportError return fit @@ -605,7 +605,7 @@ def _fetchItem(typeName, eagerCat=False): try: item = sMkt.getItem(typeName, eager=eager) except: - pyfalog.warning('EftPort: unable to fetch item "{}"'.format(typeName)) + pyfalog.warning('service.port.eft: unable to fetch item "{}"'.format(typeName)) return None if sMkt.getPublicityByItem(item): return item From c552f6a1d457bbd6c47b108789e11da4d9c351aa Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 1 Sep 2018 17:54:10 -0400 Subject: [PATCH 28/29] formatting fixes --- gui/copySelectDialog.py | 1 - gui/mainFrame.py | 2 +- service/port/eft.py | 2 +- service/port/port.py | 10 +++++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 45191f6d9..10eff2647 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -101,4 +101,3 @@ class CopySelectDialog(wx.Dialog): if v.IsChecked(): i = i ^ x return i - diff --git a/gui/mainFrame.py b/gui/mainFrame.py index ced466577..ee0396c87 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -741,7 +741,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, diff --git a/service/port/eft.py b/service/port/eft.py index fbcd4c3b2..fc8576d01 100644 --- a/service/port/eft.py +++ b/service/port/eft.py @@ -226,7 +226,6 @@ def importEft(eftString): _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: @@ -507,6 +506,7 @@ def _importPrepareString(eftString): del lines[-1] return lines + def _importGetMutationData(lines): data = {} consumedIndices = set() diff --git a/service/port/port.py b/service/port/port.py index 021b89e07..560af14ea 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -231,7 +231,7 @@ class Port(object): # Use DNA format for all other cases return "DNA", (cls.importDna(string),) - ### EFT-related methods + # EFT-related methods @staticmethod def importEft(eftString): return importEft(eftString) @@ -244,7 +244,7 @@ class Port(object): def exportEft(cls, fit, options): return exportEft(fit, options) - ### DNA-related methods + # DNA-related methods @staticmethod def importDna(string): return importDna(string) @@ -253,7 +253,7 @@ class Port(object): def exportDna(fit): return exportDna(fit) - ### ESI-related methods + # ESI-related methods @staticmethod def importESI(string): return importESI(string) @@ -262,7 +262,7 @@ class Port(object): def exportESI(fit): return exportESI(fit) - ### XML-related methods + # XML-related methods @staticmethod def importXml(text, iportuser=None): return importXml(text, iportuser) @@ -271,7 +271,7 @@ class Port(object): def exportXml(iportuser=None, *fits): return exportXml(iportuser, *fits) - ### Multibuy-related methods + # Multibuy-related methods @staticmethod def exportMultiBuy(fit): return exportMultiBuy(fit) From 6f5d0453a6fe139ba7748b299cf6ba14d4ab3911 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Sat, 1 Sep 2018 18:02:04 -0400 Subject: [PATCH 29/29] Added import of Port, which has a method that XML import uses. Importing inside the function to avoid circular import --- service/port/xml.py | 1 + 1 file changed, 1 insertion(+) diff --git a/service/port/xml.py b/service/port/xml.py index d5a5f08e0..54d5a98eb 100644 --- a/service/port/xml.py +++ b/service/port/xml.py @@ -129,6 +129,7 @@ def _resolve_module(hardware, sMkt, b_localized): 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)