From 302cab54fdae03e58b2f742f10584eb0a130bc48 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 30 Aug 2018 15:23:24 +0300 Subject: [PATCH] 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