Merge remote-tracking branch 'origin/development' into commandRefactor
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -114,6 +114,7 @@ class HandledList(list):
|
||||
|
||||
|
||||
class HandledModuleList(HandledList):
|
||||
|
||||
def append(self, mod):
|
||||
emptyPosition = float("Inf")
|
||||
for i in range(len(self)):
|
||||
@@ -131,6 +132,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:
|
||||
|
||||
@@ -20,51 +20,84 @@
|
||||
|
||||
# noinspection PyPackageRequirements
|
||||
import wx
|
||||
from service.port.eft import EFT_OPTIONS
|
||||
from service.settings import SettingsProvider
|
||||
|
||||
|
||||
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.settings = SettingsProvider.getInstance().getSettings("pyfaExport", {"format": 0, "options": 0})
|
||||
|
||||
self.copyFormat = CopySelectDialog.copyFormatEft
|
||||
selector.SetSelection(self.copyFormat)
|
||||
self.copyFormats = {
|
||||
"EFT": CopySelectDialog.copyFormatEft,
|
||||
"XML": CopySelectDialog.copyFormatXml,
|
||||
"DNA": CopySelectDialog.copyFormatDna,
|
||||
"ESI": CopySelectDialog.copyFormatEsi,
|
||||
"MultiBuy": CopySelectDialog.copyFormatMultiBuy,
|
||||
"EFS": CopySelectDialog.copyFormatEfs
|
||||
}
|
||||
|
||||
mainSizer.Add(selector, 0, wx.EXPAND | wx.ALL, 5)
|
||||
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)
|
||||
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)
|
||||
|
||||
if format == "EFT":
|
||||
bsizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
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)
|
||||
|
||||
buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
|
||||
if buttonSizer:
|
||||
mainSizer.Add(buttonSizer, 0, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
self.toggleOptions()
|
||||
self.SetSizer(mainSizer)
|
||||
self.Fit()
|
||||
self.Center()
|
||||
|
||||
def Selected(self, event):
|
||||
self.copyFormat = event.GetSelection()
|
||||
obj = event.GetEventObject()
|
||||
format = obj.GetLabel()
|
||||
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
|
||||
|
||||
def GetOptions(self):
|
||||
i = 0
|
||||
for x, v in self.options.items():
|
||||
if v.IsChecked():
|
||||
i = i ^ x
|
||||
return i
|
||||
|
||||
@@ -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.esi import ESIExportException
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -711,31 +710,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))
|
||||
|
||||
@@ -750,7 +749,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,
|
||||
@@ -759,8 +758,13 @@ class MainFrame(wx.Frame):
|
||||
dlg = CopySelectDialog(self)
|
||||
dlg.ShowModal()
|
||||
selected = dlg.GetSelected()
|
||||
options = dlg.GetOptions()
|
||||
|
||||
CopySelectDict[selected]()
|
||||
settings = SettingsProvider.getInstance().getSettings("pyfaExport")
|
||||
settings["format"] = selected
|
||||
settings["options"] = options
|
||||
|
||||
CopySelectDict[selected](options)
|
||||
|
||||
try:
|
||||
dlg.Destroy()
|
||||
|
||||
1399
service/port.py
1399
service/port.py
File diff suppressed because it is too large
Load Diff
2
service/port/__init__.py
Normal file
2
service/port/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .efs import EfsPort
|
||||
from .port import Port, IPortUser
|
||||
176
service/port/dna.py
Normal file
176
service/port/dna.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
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 + "::"
|
||||
878
service/port/eft.py
Normal file
878
service/port/eft.py
Normal file
@@ -0,0 +1,878 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import re
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
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
|
||||
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
|
||||
from service.port.shared import IPortUser, processing_notify
|
||||
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.value: {
|
||||
"name": "Implants",
|
||||
"description": "Exports implants"
|
||||
},
|
||||
Options.MUTATIONS.value: {
|
||||
"name": "Mutated Attributes",
|
||||
"description": "Exports Abyssal stats"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 = []
|
||||
|
||||
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))
|
||||
|
||||
|
||||
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<typeName>{0}+?)(,\s*(?P<chargeName>{0}+?))?(?P<offline>\s*{1})?(\s*\[(?P<mutation>\d+?)\])?$'.format(nameChars, OFFLINE_SUFFIX)
|
||||
droneCargoPattern = '^(?P<typeName>{}+?) x(?P<amount>\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(modRack))
|
||||
dummy.owner = fit
|
||||
fit.modules.appendIgnoreEmpty(dummy)
|
||||
elif m.fits(fit):
|
||||
m.owner = fit
|
||||
if not m.isValidState(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)
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _importGetMutationData(lines):
|
||||
data = {}
|
||||
consumedIndices = set()
|
||||
for i in range(len(lines)):
|
||||
line = lines[i]
|
||||
m = re.match('^\[(?P<ref>\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
|
||||
|
||||
|
||||
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<shipType>[\w\s]+),\s*(?P<fitName>.+)\]', header)
|
||||
if not m:
|
||||
pyfalog.warning('service.port.eft.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('service.port.eft.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:
|
||||
item = sMkt.getItem(typeName, eager=eager)
|
||||
except:
|
||||
pyfalog.warning('service.port.eft: unable to fetch item "{}"'.format(typeName))
|
||||
return None
|
||||
if sMkt.getPublicityByItem(item):
|
||||
return item
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _clearTail(lst):
|
||||
while lst and lst[-1] is None:
|
||||
del lst[-1]
|
||||
|
||||
|
||||
class EftImportError(Exception):
|
||||
"""Exception class emitted and consumed by EFT importer internally."""
|
||||
...
|
||||
|
||||
|
||||
class Section:
|
||||
|
||||
def __init__(self):
|
||||
self.lines = []
|
||||
self.itemSpecs = []
|
||||
self.__itemDataCats = None
|
||||
|
||||
@property
|
||||
def itemDataCats(self):
|
||||
if self.__itemDataCats is None:
|
||||
cats = set()
|
||||
for itemSpec in self.itemSpecs:
|
||||
if itemSpec is None:
|
||||
continue
|
||||
cats.add(itemSpec.item.category.name)
|
||||
self.__itemDataCats = tuple(sorted(cats))
|
||||
return self.__itemDataCats
|
||||
|
||||
@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:
|
||||
|
||||
def __init__(self, typeName):
|
||||
item = _fetchItem(typeName, eagerCat=True)
|
||||
if item is None:
|
||||
raise EftImportError
|
||||
self.typeName = typeName
|
||||
self.item = item
|
||||
|
||||
@property
|
||||
def isModule(self):
|
||||
return False
|
||||
|
||||
@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)
|
||||
self.charge = self.__fetchCharge(chargeName)
|
||||
self.offline = False
|
||||
self.mutationIdx = None
|
||||
|
||||
def __fetchCharge(self, chargeName):
|
||||
if chargeName:
|
||||
charge = _fetchItem(chargeName, eagerCat=True)
|
||||
if not charge or charge.category.name != 'Charge':
|
||||
charge = None
|
||||
else:
|
||||
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):
|
||||
|
||||
def __init__(self, typeName):
|
||||
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}
|
||||
# Other stuff
|
||||
self.mutations = {} # Format: {reference: (mutaplamid item, {attr ID: attr value})}
|
||||
|
||||
@property
|
||||
def __slotContainerMap(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 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()
|
||||
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)
|
||||
# If all the modules have same slot type, put them to appropriate
|
||||
# container with stubs
|
||||
if len(slotTypes) == 1:
|
||||
slotType = tuple(slotTypes)[0]
|
||||
self.getContainerBySlot(slotType).extend(modules)
|
||||
# Otherwise, put just modules
|
||||
else:
|
||||
for m in modules:
|
||||
if m is None:
|
||||
continue
|
||||
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.getContainerBySlot(m.slot).append(m)
|
||||
|
||||
def __makeModule(self, itemSpec):
|
||||
# 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):
|
||||
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
|
||||
208
service/port/esi.py
Normal file
208
service/port/esi.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
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'] = "<pyfa:%d />" % 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
|
||||
68
service/port/multibuy.py
Normal file
68
service/port/multibuy.py
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
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
|
||||
277
service/port/port.py
Normal file
277
service/port/port.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
import re
|
||||
import os
|
||||
import threading
|
||||
import xml.dom
|
||||
import xml.parsers.expat
|
||||
from codecs import open
|
||||
|
||||
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
|
||||
from service.port.dna import exportDna, importDna
|
||||
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
|
||||
|
||||
|
||||
pyfalog = Logger(__name__)
|
||||
|
||||
# 2017/04/05 NOTE: simple validation, for xml file
|
||||
RE_XML_START = r'<\?xml\s+version="1.0"\s*\?>'
|
||||
|
||||
|
||||
class Port(object):
|
||||
"""Service which houses all import/export format functions"""
|
||||
instance = None
|
||||
__tag_replace_flag = True
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
if cls.instance is None:
|
||||
cls.instance = Port()
|
||||
|
||||
return cls.instance
|
||||
|
||||
@classmethod
|
||||
def set_tag_replace(cls, b):
|
||||
cls.__tag_replace_flag = b
|
||||
|
||||
@classmethod
|
||||
def is_tag_replace(cls):
|
||||
# might there is a person who wants to hold tags.
|
||||
# (item link in EVE client etc. When importing again to EVE)
|
||||
return cls.__tag_replace_flag
|
||||
|
||||
@staticmethod
|
||||
def backupFits(path, iportuser):
|
||||
pyfalog.debug("Starting backup fits thread.")
|
||||
|
||||
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=backupFitsWorkerFunc,
|
||||
args=(path, iportuser)
|
||||
).start()
|
||||
|
||||
@staticmethod
|
||||
def importFitsThreaded(paths, iportuser):
|
||||
# type: (tuple, IPortUser) -> None
|
||||
"""
|
||||
:param paths: fits data file path list.
|
||||
:param iportuser: IPortUser implemented class.
|
||||
:rtype: None
|
||||
"""
|
||||
pyfalog.debug("Starting import fits thread.")
|
||||
|
||||
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=importFitsFromFileWorkerFunc,
|
||||
args=(paths, iportuser)
|
||||
).start()
|
||||
|
||||
@staticmethod
|
||||
def importFitFromFiles(paths, iportuser=None):
|
||||
"""
|
||||
Imports fits from file(s). First processes all provided paths and stores
|
||||
assembled fits into a list. This allows us to call back to the GUI as
|
||||
fits are processed as well as when fits are being saved.
|
||||
returns
|
||||
"""
|
||||
|
||||
sFit = svcFit.getInstance()
|
||||
|
||||
fit_list = []
|
||||
try:
|
||||
for path in paths:
|
||||
if iportuser: # Pulse
|
||||
msg = "Processing file:\n%s" % path
|
||||
pyfalog.debug(msg)
|
||||
processing_notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg)
|
||||
# wx.CallAfter(callback, 1, msg)
|
||||
|
||||
with open(path, "rb") as file_:
|
||||
srcString = file_.read()
|
||||
dammit = UnicodeDammit(srcString)
|
||||
srcString = dammit.unicode_markup
|
||||
|
||||
if len(srcString) == 0: # ignore blank files
|
||||
pyfalog.debug("File is blank.")
|
||||
continue
|
||||
|
||||
try:
|
||||
_, fitsImport = Port.importAuto(srcString, path, iportuser=iportuser)
|
||||
fit_list += fitsImport
|
||||
except xml.parsers.expat.ExpatError:
|
||||
pyfalog.warning("Malformed XML in:\n{0}", path)
|
||||
return False, "Malformed XML in %s" % path
|
||||
|
||||
# IDs = [] # NOTE: what use for IDs?
|
||||
numFits = len(fit_list)
|
||||
for idx, fit in enumerate(fit_list):
|
||||
# Set some more fit attributes and save
|
||||
fit.character = sFit.character
|
||||
fit.damagePattern = sFit.pattern
|
||||
fit.targetResists = sFit.targetResists
|
||||
if len(fit.implants) > 0:
|
||||
fit.implantLocation = ImplantLocation.FIT
|
||||
else:
|
||||
useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"]
|
||||
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
|
||||
db.save(fit)
|
||||
# IDs.append(fit.ID)
|
||||
if iportuser: # Pulse
|
||||
pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", idx + 1, numFits)
|
||||
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)
|
||||
)
|
||||
|
||||
except UserCancelException:
|
||||
return False, "Processing has been canceled.\n"
|
||||
except Exception as e:
|
||||
pyfalog.critical("Unknown exception processing: {0}", path)
|
||||
pyfalog.critical(e)
|
||||
# TypeError: not all arguments converted during string formatting
|
||||
# return False, "Unknown Error while processing {0}" % path
|
||||
return False, "Unknown error while processing %s\n\n Error: %s" % (path, e.message)
|
||||
|
||||
return True, fit_list
|
||||
|
||||
@staticmethod
|
||||
def importFitFromBuffer(bufferStr, activeFit=None):
|
||||
# type: (str, object) -> object
|
||||
# TODO: catch the exception?
|
||||
# activeFit is reserved?, bufferStr is unicode? (assume only clipboard string?
|
||||
sFit = svcFit.getInstance()
|
||||
_, fits = Port.importAuto(bufferStr, activeFit=activeFit)
|
||||
for fit in fits:
|
||||
fit.character = sFit.character
|
||||
fit.damagePattern = sFit.pattern
|
||||
fit.targetResists = sFit.targetResists
|
||||
if len(fit.implants) > 0:
|
||||
fit.implantLocation = ImplantLocation.FIT
|
||||
else:
|
||||
useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"]
|
||||
fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT
|
||||
db.save(fit)
|
||||
return fits
|
||||
|
||||
@classmethod
|
||||
def importAuto(cls, string, path=None, activeFit=None, iportuser=None):
|
||||
# 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()
|
||||
|
||||
# If XML-style start of tag encountered, detect as XML
|
||||
if re.search(RE_XML_START, firstLine):
|
||||
return "XML", cls.importXml(string, iportuser)
|
||||
|
||||
# If JSON-style start, parse as CREST/JSON
|
||||
if firstLine[0] == '{':
|
||||
return "JSON", (cls.importESI(string),)
|
||||
|
||||
# If we've got source file name which is used to describe ship name
|
||||
# and first line contains something like [setup name], detect as eft config file
|
||||
if re.match("\[.*\]", firstLine) and path is not None:
|
||||
filename = os.path.split(path)[1]
|
||||
shipName = filename.rsplit('.')[0]
|
||||
return "EFT Config", cls.importEftCfg(shipName, string, iportuser)
|
||||
|
||||
# If no file is specified and there's comma between brackets,
|
||||
# consider that we have [ship, setup name] and detect like eft export format
|
||||
if re.match("\[.*,.*\]", firstLine):
|
||||
return "EFT", (cls.importEft(string),)
|
||||
|
||||
# Use DNA format for all other cases
|
||||
return "DNA", (cls.importDna(string),)
|
||||
|
||||
# EFT-related methods
|
||||
@staticmethod
|
||||
def importEft(eftString):
|
||||
return importEft(eftString)
|
||||
|
||||
@staticmethod
|
||||
def importEftCfg(shipname, contents, iportuser=None):
|
||||
return importEftCfg(shipname, contents, iportuser)
|
||||
|
||||
@classmethod
|
||||
def exportEft(cls, fit, options):
|
||||
return exportEft(fit, options)
|
||||
|
||||
# DNA-related methods
|
||||
@staticmethod
|
||||
def importDna(string):
|
||||
return importDna(string)
|
||||
|
||||
@staticmethod
|
||||
def exportDna(fit):
|
||||
return exportDna(fit)
|
||||
|
||||
# ESI-related methods
|
||||
@staticmethod
|
||||
def importESI(string):
|
||||
return importESI(string)
|
||||
|
||||
@staticmethod
|
||||
def exportESI(fit):
|
||||
return exportESI(fit)
|
||||
|
||||
# XML-related methods
|
||||
@staticmethod
|
||||
def importXml(text, iportuser=None):
|
||||
return importXml(text, iportuser)
|
||||
|
||||
@staticmethod
|
||||
def exportXml(iportuser=None, *fits):
|
||||
return exportXml(iportuser, *fits)
|
||||
|
||||
# Multibuy-related methods
|
||||
@staticmethod
|
||||
def exportMultiBuy(fit):
|
||||
return exportMultiBuy(fit)
|
||||
70
service/port/shared.py
Normal file
70
service/port/shared.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
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
|
||||
326
service/port/xml.py
Normal file
326
service/port/xml.py
Normal file
@@ -0,0 +1,326 @@
|
||||
# =============================================================================
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
# =============================================================================
|
||||
|
||||
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'<localized hint="([^"]+)">([^\*]+)\*</localized>')
|
||||
|
||||
|
||||
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
|
||||
# <localized hint="Maelstrom">Maelstrom</localized>
|
||||
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):
|
||||
from .port import Port
|
||||
# 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 <br> 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)+", "<br>", 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()
|
||||
Reference in New Issue
Block a user