diff --git a/eos/db/__init__.py b/eos/db/__init__.py index 09a092c81..789141435 100755 --- a/eos/db/__init__.py +++ b/eos/db/__init__.py @@ -66,7 +66,7 @@ from eos.db.saveddata.queries import getUser, getCharacter, getFit, getFitsWithS getCharacterList, getPrice, getDamagePatternList, getDamagePattern, \ getFitList, getFleetList, getFleet, save, remove, commit, add, \ getCharactersForUser, getMiscData, getSquadsIDsWithFitID, getWing, \ - getSquad, getBoosterFits + getSquad, getBoosterFits, getProjectedFits #If using in memory saveddata, you'll want to reflect it so the data structure is good. if config.saveddata_connectionstring == "sqlite:///:memory:": diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 01c2888ce..ddb8d53ac 100755 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -21,6 +21,7 @@ from eos.db.util import processEager, processWhere from eos.db import saveddata_session, sd_lock from eos.types import User, Character, Fit, Price, DamagePattern, Fleet, MiscData, Wing, Squad from eos.db.saveddata.fleet import squadmembers_table +from eos.db.saveddata.fit import projectedFits_table from sqlalchemy.sql import and_ import eos.config @@ -360,7 +361,15 @@ def getSquadsIDsWithFitID(fitID): return squads else: raise TypeError("Need integer as argument") - + +def getProjectedFits(fitID): + if isinstance(fitID, int): + with sd_lock: + filter = and_(projectedFits_table.c.sourceID == fitID, Fit.ID == projectedFits_table.c.victimID) + fits = saveddata_session.query(Fit).filter(filter).all() + return fits + else: + raise TypeError("Need integer as argument") def add(stuff): with sd_lock: diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 418053843..54960b4bb 100755 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -196,10 +196,12 @@ class Fit(object): def importEft(cls, eftString): from eos import db offineSuffix = " /OFFLINE" + fit = cls() eftString = eftString.strip() lines = re.split('[\n\r]+', eftString) info = lines[0][1:-1].split(",", 1) + if len(info) == 2: shipType = info[0].strip() fitName = info[1].strip() @@ -212,28 +214,46 @@ class Fit(object): fit.name = fitName except: return + + # maintain map of drones and their quantities droneMap = {} for i in range(1, len(lines)): + ammoName = None + droneAmount = None + line = lines[i].strip() if not line: continue + setOffline = line.endswith(offineSuffix) if setOffline == True: + # remove offline suffix from line line = line[:len(line) - len(offineSuffix)] + modAmmo = line.split(",") - modDrone = modAmmo[0].split(" x") - if len(modAmmo) == 2: ammoName = modAmmo[1].strip() - else: ammoName = None - modName = modDrone[0].strip() - if len(modDrone) == 2: droneAmount = modDrone[1].strip() - else: droneAmount = None + # matches drone and cargo with x{qty} + modExtra = modAmmo[0].split(" x") + + if len(modAmmo) == 2: + # line with a module and ammo + ammoName = modAmmo[1].strip() + modName = modAmmo[0].strip() + elif len(modExtra) == 2: + # line with drone/cargo and qty + droneAmount = modExtra[1].strip() + modName = modExtra[0].strip() + else: + # line with just module + modName = modExtra[0].strip() + try: + # get item information. If we are on a Drone/Cargo line, throw out cargo item = db.getItem(modName, eager="group.category") - except: - try: - item = db.getItem(modAmmo[0], eager="group.category") - except: + if len(modExtra) == 2 and item.category.name != "Drone": continue + except: + # if no data can be found (old names) + continue if item.category.name == "Drone": droneAmount = int(droneAmount) if droneAmount is not None else 1 @@ -249,7 +269,9 @@ class Fit(object): continue if ammoName: try: - m.charge = db.getItem(ammoName) + ammo = db.getItem(ammoName) + if m.isValidCharge(ammo) and m.charge is None: + m.charge = ammo except: pass @@ -442,9 +464,7 @@ class Fit(object): f.modules.append(m) except KeyboardInterrupt: continue - - fits.append(f) - + fits.append(f) return fits @@ -496,18 +516,28 @@ class Fit(object): def exportDna(self): dna = str(self.shipID) mods = OrderedDict() + charges = OrderedDict() for mod in self.modules: if not mod.isEmpty: if not mod.itemID in mods: mods[mod.itemID] = 0 mods[mod.itemID] += 1 + if mod.charge: + if not mod.chargeID in charges: + charges[mod.chargeID] = 0 + # `or 1` because some charges (ie scripts) are without qty + charges[mod.chargeID] += mod.numShots or 1 + for mod in mods: dna += ":{0};{1}".format(mod, mods[mod]) for drone in self.drones: dna += ":{0};{1}".format(drone.itemID, drone.amount) + for charge in charges: + dna += ":{0};{1}".format(charge, charges[charge]) + return dna + "::" @classmethod @@ -515,7 +545,6 @@ class Fit(object): doc = xml.dom.minidom.Document() fittings = doc.createElement("fittings") doc.appendChild(fittings) - for fit in fits: fitting = doc.createElement("fitting") fitting.setAttribute("name", fit.name) @@ -527,6 +556,7 @@ class Fit(object): shipType.setAttribute("value", fit.ship.item.name) fitting.appendChild(shipType) + charges = {} slotNum = {} for module in fit.modules: if module.isEmpty: @@ -543,6 +573,12 @@ class Fit(object): hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId)) fitting.appendChild(hardware) + if module.charge: + if not module.charge.name in charges: + charges[module.charge.name] = 0 + # `or 1` because some charges (ie scripts) are without qty + charges[module.charge.name] += module.numShots or 1 + for drone in fit.drones: hardware = doc.createElement("hardware") hardware.setAttribute("qty", "%d" % drone.amount) @@ -550,13 +586,19 @@ class Fit(object): hardware.setAttribute("type", drone.item.name) fitting.appendChild(hardware) + for name, qty in charges.items(): + hardware = doc.createElement("hardware") + hardware.setAttribute("qty", "%d" % qty) + hardware.setAttribute("slot", "cargo") + hardware.setAttribute("type", name) + fitting.appendChild(hardware) + for cargo in fit.cargo: hardware = doc.createElement("hardware") hardware.setAttribute("qty", "%d" % cargo.amount) hardware.setAttribute("slot", "cargo") hardware.setAttribute("type", cargo.item.name) fitting.appendChild(hardware) - return doc.toprettyxml() @reconstructor diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index ad4a52811..bde1a9aa6 100755 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -569,7 +569,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): context = ("module",) projected = False - if self.charge is not None and not projected: + if self.charge is not None: for effect in self.charge.effects.itervalues(): if effect.runTime == runTime: effect.handler(fit, self, ("moduleCharge",)) diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index 6a0118011..05cd99c02 100755 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -22,6 +22,10 @@ from eos.effectHandlerHelpers import HandledItem class Ship(ItemAttrShortcut, HandledItem): def __init__(self, item): + + if item.category.name != "Ship": + raise ValueError('Passed item "%s" (category: (%s)) is not under Ship category'%(item.name, item.category.name)) + self.__item = item self.__itemModifiedAttributes = ModifiedAttributeDict() if not isinstance(item, int): diff --git a/eos/utils/scripts/jsonToSql.py b/eos/utils/scripts/jsonToSql.py index bc4136405..01cbfeca6 100755 --- a/eos/utils/scripts/jsonToSql.py +++ b/eos/utils/scripts/jsonToSql.py @@ -99,6 +99,9 @@ if __name__ == "__main__": # We don't care about some kind of rows, filter it out if so if not isIgnored(jsonName, row): instance = tables[jsonName]() + # fix for issue 80 + if jsonName is "icons" and "res:/UI/Texture/Icons/" in str(row['iconFile']): + row['iconFile'] = row['iconFile'].replace('res:/UI/Texture/Icons/','').replace('.png','') for k, v in row.iteritems(): setattr(instance, fieldMap.get(k, k), v) diff --git a/gui/aboutData.py b/gui/aboutData.py index 0d1b029b5..69fa96d51 100644 --- a/gui/aboutData.py +++ b/gui/aboutData.py @@ -19,8 +19,16 @@ import config versionString = "{0} {1} - {2} {3}".format(config.version, config.tag, config.expansionName, config.expansionVersion) -license = "pyfa is released under GNU GPLv3" -licenseLocation = "gpl.txt" -developers = ("\n cncfanatics \t(Sakari Orisi)\n" , " DarkPhoenix \t(Kadesh Priestess)\n", " Darriele \t(Darriele)") -credits = (("Entity (Entity) \t\tCapacitor calculations / EVEAPI python lib / Reverence"), ("Aurora \t\tMaths"), ("Corollax (Aamrr) \tVarious EOS/pyfa improvements")) -description = "Pyfa (the Python Fitting Assistant) is a standalone application able to create and simulate fittings for EVE-Online SciFi MMORPG with a very high degree of accuracy.\nPyfa can be virtually ran on all platforms where python and wxwidgets are supported.\n\n\nAll EVE-Online related materials are property of CCP hf.\n\nSilk Icons Set by famfamfam.com released under Creative Commons Attribution 2.5 License\n\nFat Cow Icons by fatcow.com released under Creative Commons Attribution 3.0 License" +licenses = ( + "pyfa is released under GNU GPLv3 - see included gpl.txt", + "All EVE-Online related materials are property of CCP hf.", + "Silk Icons Set by famfamfam.com - Creative Commons Attribution 2.5 License", + "Fat Cow Icons by fatcow.com - Creative Commons Attribution 3.0 License" +) +developers = ("blitzmann \t(Sable Blitzmann)", "cncfanatics \t(Sakari Orisi)" , "DarkPhoenix \t(Kadesh Priestess) (Project Lead)", "Darriele \t(Darriele)") +credits = ("Entity (Entity) \t\tCapacitor calculations / EVEAPI python lib / Reverence", "Aurora \t\t\tMaths", "Corollax (Aamrr) \tVarious EOS / pyfa improvements") +description = ( + "Pyfa (the Python Fitting Assistant) is an open-source standalone application able to " + "create and simulate fittings for EVE-Online SciFi MMORPG with a very high degree of " + "accuracy. Pyfa can run on all platforms where Python and wxWidgets are supported." +) diff --git a/gui/builtinContextMenus/moduleAmmoPicker.py b/gui/builtinContextMenus/moduleAmmoPicker.py index 6f69f90d2..7f11207cc 100644 --- a/gui/builtinContextMenus/moduleAmmoPicker.py +++ b/gui/builtinContextMenus/moduleAmmoPicker.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from gui.contextMenu import ContextMenu import gui.mainFrame import service @@ -112,7 +113,7 @@ class ModuleAmmoPicker(ContextMenu): def addSeperator(self, m, text): id = wx.NewId() - m.Append(id, "--- %s ---" % text) + m.Append(id, u'─ %s ─' % text) m.Enable(id, False) def getSubMenu(self, context, selection, menu, i): @@ -132,6 +133,11 @@ class ModuleAmmoPicker(ContextMenu): sub = None self.charges.sort(key=self.turretSorter) for charge in self.charges: + # fix issue 71 - will probably have to change if CCP adds more Orbital ammo + if "Orbital" in charge.name: + item = self.addCharge(m, charge) + items.append(item) + continue currBase = charge.name.rsplit()[-2:] currRange = charge.getAttribute("weaponRangeMultiplier") if nameBase is None or range != currRange or nameBase != currBase: @@ -156,6 +162,7 @@ class ModuleAmmoPicker(ContextMenu): if sub is not None: self.addSeperator(sub, "More Damage") + for item in items: m.AppendItem(item) diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index 359ed7751..bc407da4e 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -233,10 +233,23 @@ class FittingView(d.Display): event.Skip() def fitRemoved(self, event): + '''If fit is removed and active, the page is deleted. + We also refresh the fit of the new current page in case + delete fit caused change in stats (projected) + ''' fitID = event.fitID + if fitID == self.getActiveFit(): self.parent.DeletePage(self.parent.GetPageIndex(self)) - + + try: + # Sometimes there is no active page after deletion, hence the try block + cFit = service.Fit.getInstance() + cFit.refreshFit(self.getActiveFit()) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID)) + except wx._core.PyDeadObjectError: + pass + event.Skip() def fitRenamed(self, event): diff --git a/gui/itemStats.py b/gui/itemStats.py index 791c2eab4..6f4d1c6fa 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -26,9 +26,13 @@ import wx.lib.mixins.listctrl as listmix import wx.html from eos.types import Ship, Module, Skill, Booster, Implant, Drone from gui.utils.numberFormatter import formatAmount -from collections import OrderedDict import service +try: + from collections import OrderedDict +except ImportError: + from gui.utils.compat import OrderedDict + class ItemStatsDialog(wx.Dialog): counter = 0 def __init__(self, victim, fullContext=None, pos = wx.DefaultPosition, size = wx.DefaultSize, maximized = False): diff --git a/gui/mainFrame.py b/gui/mainFrame.py index a1f82b15d..355999ec9 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -243,14 +243,12 @@ class MainFrame(wx.Frame): info = wx.AboutDialogInfo() info.Name = "pyfa" info.Version = gui.aboutData.versionString - info.Description = wordwrap(gui.aboutData.description + "\n\n\nDevelopers: " + - "".join(gui.aboutData.developers) + - "\n\nAdditional credits:\n " + - "\n ".join(gui.aboutData.credits) - + "\n\nLicense: " + - gui.aboutData.license + - " - see included " + - gui.aboutData.licenseLocation + + info.Description = wordwrap(gui.aboutData.description + "\n\nDevelopers:\n\t" + + "\n\t".join(gui.aboutData.developers) + + "\n\nAdditional credits:\n\t" + + "\n\t".join(gui.aboutData.credits) + + "\n\nLicenses:\n\t" + + "\n\t".join(gui.aboutData.licenses) + "\n\nPython: \t" + sys.version + "\nwxPython: \t" + wx.__version__ + "\nSQLAlchemy: \t" + sqlalchemy.__version__, @@ -364,7 +362,7 @@ class MainFrame(wx.Frame): # Export HTML self.Bind(wx.EVT_MENU, self.exportHtml, id=menuBar.exportHtmlId) # Preference dialog - self.Bind(wx.EVT_MENU, self.showPreferenceDialog, id = menuBar.preferencesId) + self.Bind(wx.EVT_MENU, self.showPreferenceDialog, id=wx.ID_PREFERENCES) # User guide self.Bind(wx.EVT_MENU, self.goWiki, id = menuBar.wikiId) # EVE Forums @@ -384,6 +382,7 @@ class MainFrame(wx.Frame): self.additionstab2 = wx.NewId() self.additionstab3 = wx.NewId() self.additionstab4 = wx.NewId() + self.additionstab5 = wx.NewId() # Close Page self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId) @@ -396,6 +395,7 @@ class MainFrame(wx.Frame): self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab2) self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab3) self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab4) + self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab5) actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId), (wx.ACCEL_CMD, ord('T'), self.addPageId), @@ -416,10 +416,12 @@ class MainFrame(wx.Frame): (wx.ACCEL_CTRL, ord('2'), self.additionstab2), (wx.ACCEL_CTRL, ord('3'), self.additionstab3), (wx.ACCEL_CTRL, ord('4'), self.additionstab4), + (wx.ACCEL_CTRL, ord('5'), self.additionstab5), (wx.ACCEL_CMD, ord('1'), self.additionstab1), (wx.ACCEL_CMD, ord('2'), self.additionstab2), (wx.ACCEL_CMD, ord('3'), self.additionstab3), - (wx.ACCEL_CMD, ord('4'), self.additionstab4) + (wx.ACCEL_CMD, ord('4'), self.additionstab4), + (wx.ACCEL_CMD, ord('5'), self.additionstab5) ] atable = wx.AcceleratorTable(actb) self.SetAcceleratorTable(atable) @@ -434,7 +436,8 @@ class MainFrame(wx.Frame): selTab = 2 if event.GetId() == self.additionstab4: selTab = 3 - + if event.GetId() == self.additionstab5: + selTab = 4 if selTab is not None: self.additionsPane.notebook.SetSelection(selTab) diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index ffef67857..cdfa9108c 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -33,7 +33,6 @@ class MainMenuBar(wx.MenuBar): self.exportSkillsNeededId = wx.NewId() self.importCharacterId = wx.NewId() self.exportHtmlId = wx.NewId() - self.preferencesId = wx.NewId() self.wikiId = wx.NewId() self.forumId = wx.NewId() @@ -59,7 +58,6 @@ class MainMenuBar(wx.MenuBar): fileMenu.AppendSeparator() fileMenu.Append(wx.ID_EXIT) - # Edit menu editMenu = wx.Menu() self.Append(editMenu, "&Edit") @@ -67,7 +65,6 @@ class MainMenuBar(wx.MenuBar): #editMenu.Append(wx.ID_UNDO) #editMenu.Append(wx.ID_REDO) - copyText = "&To Clipboard" + ("\tCTRL+C" if 'wxMSW' in wx.PlatformInfo else "") pasteText = "&From Clipboard" + ("\tCTRL+V" if 'wxMSW' in wx.PlatformInfo else "") editMenu.Append(wx.ID_COPY, copyText, "Export a fit to the clipboard") @@ -89,13 +86,9 @@ class MainMenuBar(wx.MenuBar): graphFrameItem.SetBitmap(bitmapLoader.getBitmap("graphs_small", "icons")) windowMenu.AppendItem(graphFrameItem) - #======================================================================= - # DISABLED FOR RC2 Release - # - preferencesItem = wx.MenuItem(windowMenu, self.preferencesId, "Preferences\tCTRL+P") + preferencesItem = wx.MenuItem(windowMenu, wx.ID_PREFERENCES, "Preferences\tCTRL+P") preferencesItem.SetBitmap(bitmapLoader.getBitmap("preferences_small", "icons")) windowMenu.AppendItem(preferencesItem) - #======================================================================= # Help menu helpMenu = wx.Menu() @@ -108,8 +101,6 @@ class MainMenuBar(wx.MenuBar): if config.debug: helpMenu.Append( self.mainFrame.widgetInspectMenuID, "Open Widgets Inspect tool", "Open Widgets Inspect tool") - - self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) def fitChanged(self, event): diff --git a/gui/utils/exportHtml.py b/gui/utils/exportHtml.py index 049d3535a..061728fa4 100644 --- a/gui/utils/exportHtml.py +++ b/gui/utils/exportHtml.py @@ -132,20 +132,28 @@ class exportHtmlThread(threading.Thread): if len(fits) > 0: groupFits += len(fits) - # Ship group header - HTMLship = ( - '
  • \n' - '

    ' + ship.name + ' '+str(len(fits))+'

    \n' - ' \n' + '
  • \n') if groupFits > 0: # Market group header HTML += ( diff --git a/service/fit.py b/service/fit.py index c3589ee9d..5e6bdacd5 100644 --- a/service/fit.py +++ b/service/fit.py @@ -162,6 +162,8 @@ class Fit(object): fit = eos.db.getFit(fitID) sFlt = Fleet.getInstance() sFlt.removeAssociatedFleetData(fit) + self.removeProjectedData(fitID) + eos.db.remove(fit) def copyFit(self, fitID): @@ -177,7 +179,15 @@ class Fit(object): fit = eos.db.getFit(fitID) fit.clear() return fit - + + def removeProjectedData(self, fitID): + '''Removes projection relation from ships that have fitID as projection. See GitHub issue #90''' + fit = eos.db.getFit(fitID) + fits = eos.db.getProjectedFits(fitID) + + for projectee in fits: + projectee.projectedFits.remove(fit) + def toggleFactorReload(self, fitID): if fitID is None: return None diff --git a/staticdata/icons/icon108_5.png b/staticdata/icons/icon108_5.png new file mode 100644 index 000000000..0b96f8ce4 Binary files /dev/null and b/staticdata/icons/icon108_5.png differ diff --git a/staticdata/icons/icon113_64_1.png b/staticdata/icons/icon113_64_1.png new file mode 100644 index 000000000..c54aac031 Binary files /dev/null and b/staticdata/icons/icon113_64_1.png differ diff --git a/staticdata/icons/icon113_64_2.png b/staticdata/icons/icon113_64_2.png new file mode 100644 index 000000000..604e32b4a Binary files /dev/null and b/staticdata/icons/icon113_64_2.png differ diff --git a/staticdata/icons/icon113_64_3.png b/staticdata/icons/icon113_64_3.png new file mode 100644 index 000000000..729ab1e72 Binary files /dev/null and b/staticdata/icons/icon113_64_3.png differ diff --git a/staticdata/icons/icon53_64_16.png b/staticdata/icons/icon53_64_16.png new file mode 100644 index 000000000..42afb7b32 Binary files /dev/null and b/staticdata/icons/icon53_64_16.png differ diff --git a/staticdata/icons/icon94_64_9.png b/staticdata/icons/icon94_64_9.png new file mode 100644 index 000000000..acd08557b Binary files /dev/null and b/staticdata/icons/icon94_64_9.png differ diff --git a/staticdata/icons/iconMarketIcon_16px_Amarr.png b/staticdata/icons/iconMarketIcon_16px_Amarr.png new file mode 100644 index 000000000..7c8626a8e Binary files /dev/null and b/staticdata/icons/iconMarketIcon_16px_Amarr.png differ diff --git a/staticdata/icons/iconMarketIcon_16px_Caldari.png b/staticdata/icons/iconMarketIcon_16px_Caldari.png new file mode 100644 index 000000000..a35a5328a Binary files /dev/null and b/staticdata/icons/iconMarketIcon_16px_Caldari.png differ diff --git a/staticdata/icons/iconMarketIcon_16px_Gallente.png b/staticdata/icons/iconMarketIcon_16px_Gallente.png new file mode 100644 index 000000000..ee62d79b4 Binary files /dev/null and b/staticdata/icons/iconMarketIcon_16px_Gallente.png differ diff --git a/staticdata/icons/iconMarketIcon_16px_Minmatar.png b/staticdata/icons/iconMarketIcon_16px_Minmatar.png new file mode 100644 index 000000000..56c17d248 Binary files /dev/null and b/staticdata/icons/iconMarketIcon_16px_Minmatar.png differ