diff --git a/eos/db/saveddata/cart.py b/eos/db/saveddata/cart.py new file mode 100644 index 0000000000..8e141176ff --- /dev/null +++ b/eos/db/saveddata/cart.py @@ -0,0 +1,41 @@ +# =============================================================================== +# Copyright (C) 2010 Diego Duclos +# +# This file is part of eos. +# +# eos is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# eos 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with eos. If not, see . +# =============================================================================== + +from sqlalchemy import Table, Column, Integer, ForeignKey, DateTime +from sqlalchemy.orm import mapper, relation +import datetime + +from eos.db import saveddata_meta +from eos.saveddata.cargo import Cargo +from eos.saveddata.fit import Fit + +cart_table = Table("cart", saveddata_meta, + Column("ID", Integer, primary_key=True), + Column("fitID", Integer, ForeignKey("fits.ID"), nullable=False, index=True), + Column("itemID", Integer, nullable=False), + Column("amount", Integer, nullable=False), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), + ) + +mapper(Cargo, cart_table, + properties={ + "owner": relation(Fit) + } +) diff --git a/eos/saveddata/cart.py b/eos/saveddata/cart.py new file mode 100644 index 0000000000..917cdb61b4 --- /dev/null +++ b/eos/saveddata/cart.py @@ -0,0 +1,101 @@ +# =============================================================================== +# Copyright (C) 2010 Diego Duclos +# +# This file is part of eos. +# +# eos is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# eos 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with eos. If not, see . +# =============================================================================== + +import sys +from logbook import Logger + +from sqlalchemy.orm import validates, reconstructor + +import eos.db +from eos.effectHandlerHelpers import HandledItem +from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut + +pyfalog = Logger(__name__) + + +class Cart(HandledItem, ItemAttrShortcut): + def __init__(self, item): + """Initialize cart from the program""" + self.__item = item + self.itemID = item.ID if item is not None else None + self.amount = 0 + self.__itemModifiedAttributes = ModifiedAttributeDict() + self.__itemModifiedAttributes.original = item.attributes + self.__itemModifiedAttributes.overrides = item.overrides + + @reconstructor + def init(self): + """Initialize cart from the database and validate""" + self.__item = None + + if self.itemID: + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + pyfalog.error("Item (id: {0}) does not exist", self.itemID) + return + + self.__itemModifiedAttributes = ModifiedAttributeDict() + self.__itemModifiedAttributes.original = self.__item.attributes + self.__itemModifiedAttributes.overrides = self.__item.overrides + + @property + def itemModifiedAttributes(self): + return self.__itemModifiedAttributes + + @property + def isInvalid(self): + return self.__item is None + + @property + def item(self): + return self.__item + + def clear(self): + self.itemModifiedAttributes.clear() + + @validates("fitID", "itemID", "amount") + def validator(self, key, val): + map = { + "fitID" : lambda _val: isinstance(_val, int), + "itemID": lambda _val: isinstance(_val, int), + "amount": lambda _val: isinstance(_val, int) + } + + if key == "amount" and val > sys.maxsize: + val = sys.maxsize + + if not map[key](val): + raise ValueError(str(val) + " is not a valid value for " + key) + else: + return val + + def __deepcopy__(self, memo): + copy = Cart(self.item) + copy.amount = self.amount + return copy + + def rebase(self, item): + amount = self.amount + Cart.__init__(self, item) + self.amount = amount + + def __repr__(self): + return "Cart(ID={}, name={}) at {}".format( + self.item.ID, self.item.name, hex(id(self)) + ) diff --git a/eos/saveddata/damagePattern.py b/eos/saveddata/damagePattern.py index 6f3ec94322..b4bb12c140 100644 --- a/eos/saveddata/damagePattern.py +++ b/eos/saveddata/damagePattern.py @@ -59,38 +59,6 @@ def _c(x): (-20, (_c(_t('Exotic Plasma')) + _t('Baryon'), 0, 59737, 0, 40263)), (-21, (_c(_t('Exotic Plasma')) + _t('Tetryon'), 0, 69208, 0, 30792)), (-22, (_c(_t('Exotic Plasma')) + '|' + _t('[T2] Occult'), 0, 55863, 0, 44137)), - (-23, (_c(_t('Hybrid Charges')) + '|' + _t('[T2] Spike'), 0, 4, 4, 0)), - (-24, (_c(_t('Hybrid Charges')) + '|' + _t('[T2] Null'), 0, 6, 5, 0)), - (-25, (_c(_t('Hybrid Charges')) + _t('Iron'), 0, 2, 3, 0)), - (-26, (_c(_t('Hybrid Charges')) + _t('Tungsten'), 0, 2, 4, 0)), - (-27, (_c(_t('Hybrid Charges')) + _t('Iridium'), 0, 3, 4, 0)), - (-28, (_c(_t('Hybrid Charges')) + _t('Lead'), 0, 3, 5, 0)), - (-29, (_c(_t('Hybrid Charges')) + _t('Thorium'), 0, 4, 5, 0)), - (-30, (_c(_t('Hybrid Charges')) + _t('Uranium'), 0, 4, 6, 0)), - (-31, (_c(_t('Hybrid Charges')) + _t('Plutonium'), 0, 5, 6, 0)), - (-32, (_c(_t('Hybrid Charges')) + _t('Antimatter'), 0, 5, 7, 0)), - (-33, (_c(_t('Hybrid Charges')) + '|' + _t('[T2] Javelin'), 0, 8, 6, 0)), - (-34, (_c(_t('Hybrid Charges')) + '|' + _t('[T2] Void'), 0, 7.7, 7.7, 0)), - (-35, (_c(_t('Projectile Ammo')) + '|' + _t('[T2] Tremor'), 0, 0, 3, 5)), - (-36, (_c(_t('Projectile Ammo')) + '|' + _t('[T2] Barrage'), 0, 0, 5, 6)), - (-37, (_c(_t('Projectile Ammo')) + _t('Carbonized Lead'), 0, 0, 4, 1)), - (-38, (_c(_t('Projectile Ammo')) + _t('Nuclear'), 0, 0, 1, 4)), - (-39, (_c(_t('Projectile Ammo')) + _t('Proton'), 3, 0, 2, 0)), - (-40, (_c(_t('Projectile Ammo')) + _t('Depleted Uranium'), 0, 3, 2, 3)), - (-41, (_c(_t('Projectile Ammo')) + _t('Titanium Sabot'), 0, 0, 6, 2)), - (-42, (_c(_t('Projectile Ammo')) + _t('EMP'), 9, 0, 1, 2)), - (-43, (_c(_t('Projectile Ammo')) + _t('Phased Plasma'), 0, 10, 2, 0)), - (-44, (_c(_t('Projectile Ammo')) + _t('Fusion'), 0, 0, 2, 10)), - (-45, (_c(_t('Projectile Ammo')) + '|' + _t('[T2] Quake'), 0, 0, 5, 9)), - (-46, (_c(_t('Projectile Ammo')) + '|' + _t('[T2] Hail'), 0, 0, 3.3, 12.1)), - (-47, (_c(_t('Missiles')) + _t('Mjolnir'), 1, 0, 0, 0)), - (-48, (_c(_t('Missiles')) + _t('Inferno'), 0, 1, 0, 0)), - (-49, (_c(_t('Missiles')) + _t('Scourge'), 0, 0, 1, 0)), - (-50, (_c(_t('Missiles')) + _t('Nova'), 0, 0, 0, 1)), - (-51, (_c(_t('Bombs')) + _t('Electron Bomb'), 6400, 0, 0, 0)), - (-52, (_c(_t('Bombs')) + _t('Scorch Bomb'), 0, 6400, 0, 0)), - (-53, (_c(_t('Bombs')) + _t('Concussion Bomb'), 0, 0, 6400, 0)), - (-54, (_c(_t('Bombs')) + _t('Shrapnel Bomb'), 0, 0, 0, 6400)), # Source: ticket #2067 and #2265 (-55, (_c(_t('NPC')) + _c(_t('Abyssal')) + _t('All'), 126, 427, 218, 230)), (-109, (_c(_t('NPC')) + _c(_t('Abyssal')) + _t('Angel'), 450, 72, 80, 398)), diff --git a/eve.db b/eve.db new file mode 100644 index 0000000000..3b357c781c Binary files /dev/null and b/eve.db differ diff --git a/gui/additionsPane.py b/gui/additionsPane.py index c7c8eb3761..ee54789777 100644 --- a/gui/additionsPane.py +++ b/gui/additionsPane.py @@ -29,6 +29,7 @@ from gui.builtinAdditionPanes.fighterView import FighterView from gui.builtinAdditionPanes.implantView import ImplantView from gui.builtinAdditionPanes.notesView import NotesView +from gui.builtinAdditionPanes.cartView import CartView from gui.builtinAdditionPanes.projectedView import ProjectedView from gui.chrome_tabs import ChromeNotebook from gui.toggle_panel import TogglePanel @@ -61,6 +62,7 @@ def __init__(self, parent, mainFrame): gangImg = BitmapLoader.getImage("fleet_fc_small", "gui") cargoImg = BitmapLoader.getImage("cargo_small", "gui") notesImg = BitmapLoader.getImage("skill_small", "gui") + cartImg = BitmapLoader.getImage("cart_small", "gui") self.drone = DroneView(self.notebook) self.notebook.AddPage(self.drone, _t("Drones"), image=droneImg, closeable=False) @@ -86,12 +88,16 @@ def __init__(self, parent, mainFrame): self.notes = NotesView(self.notebook) self.notebook.AddPage(self.notes, _t("Notes"), image=notesImg, closeable=False) + + self.cart = CartView(self.notebook) + self.notebook.AddPage(self.cart, _t("Cart"), image=cartImg, closeable=False) + self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged) self.mainFrame.Bind(GE.FIT_NOTES_CHANGED, self.OnNotesChanged) self.notebook.SetSelection(0) - PANES = ["Drones", "Fighters", "Cargo", "Implants", "Boosters", "Projected", "Command", "Notes"] + PANES = ["Drones", "Fighters", "Cargo", "Implants", "Boosters", "Projected", "Command", "Notes", "Cart"] def select(self, name, focus=True): self.notebook.SetSelection(self.PANES.index(name), focus=focus) diff --git a/gui/builtinAdditionPanes/cartView.py b/gui/builtinAdditionPanes/cartView.py new file mode 100644 index 0000000000..501e165847 --- /dev/null +++ b/gui/builtinAdditionPanes/cartView.py @@ -0,0 +1,267 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + +# noinspection PyPackageRequirements +import wx + +import gui.display as d +import gui.fitCommands as cmd +import gui.globalEvents as GE +from gui.contextMenu import ContextMenu +from gui.builtinMarketBrowser.events import ITEM_SELECTED, ItemSelected +from gui.utils.staticHelpers import DragDropHelper +from service.fit import Fit +from service.market import Market +from service.port import Port +from service.port.esi import ESIExportException + + +_t = wx.GetTranslation + +class CartViewDrop(wx.DropTarget): + def __init__(self, dropFn, *args, **kwargs): + super(CartViewDrop, self).__init__(*args, **kwargs) + self.dropFn = dropFn + #this is really transferring an EVE itemID + self.dropData = wx.TextDataObject() + self.SetDataObject(self.dropData) + + def OnData(self, x, y, t): + if self.GetData(): + dragged_data = DragDropHelper.data + data = dragged_data.split(':') + self.dropFn(x, y, data) + return t + + +class CartView(d.Display): + DEFAULT_COLS = ["Base Icon", + "Base Name", + "attr:volume", + "Price"] + + def __init__(self, parent): + d.Display.__init__(self, parent, style=wx.BORDER_NONE) + + self.lastFitId = None + + self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) + self.mainFrame.Bind(ITEM_SELECTED, self.addItem) + self.Bind(wx.EVT_LEFT_DCLICK, self.onLeftDoubleClick) + self.Bind(wx.EVT_KEY_UP, self.kbEvent) + self.SetDropTarget(CartViewDrop(self.handleListDrag)) + self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag) + + self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu) + + + + + + def addItem(self, event): + newFit = "cartBackGroundFit.xml" + item = Market.getInstance().getItem(event.itemID, eager='group') + if item is None or not (item.isCharge or item.isCommodity): + event.Skip() + return + + fitID = self.mainFrame.getActiveFit() + fit = Fit.getInstance().getFit(fitID) + + if not fit: + event.Skip() + return + modifiers = wx.GetMouseState().GetModifiers() + amount = 1 + if modifiers == wx.MOD_CONTROL: + amount = 10 + elif modifiers == wx.MOD_ALT: + amount = 100 + elif modifiers == wx.MOD_CONTROL | wx.MOD_ALT: + amount = 1000 + self.mainFrame.command.Submit(cmd.GuiAddCartCommand( + fitID=fitID, itemID=item.ID, amount=amount)) + self.mainFrame.additionsPane.select('Cart') + event.Skip() + def handleListDrag(self, x, y, data): + """ + Handles dragging of items from various pyfa displays which support it + + data is list with two indices: + data[0] is hard-coded str of originating source + data[1] is typeID or index of data we want to manipulate + """ + + if data[0] == "fitting": + self.swapModule(x, y, int(data[1])) + elif data[0] == "market": + fitID = self.mainFrame.getActiveFit() + if fitID: + self.mainFrame.command.Submit(cmd.GuiAddCartCommand( + fitID=fitID, itemID=int(data[1]), amount=1)) + + def startDrag(self, event): + row = event.GetIndex() + + if row != -1: + data = wx.TextDataObject() + try: + dataStr = "cart:{}".format(self.cart[row].itemID) + except IndexError: + return + data.SetText(dataStr) + + self.unselectAll() + self.Select(row, True) + + dropSource = wx.DropSource(self) + dropSource.SetData(data) + DragDropHelper.data = dataStr + dropSource.DoDragDrop() + + def kbEvent(self, event): + keycode = event.GetKeyCode() + modifiers = event.GetModifiers() + if keycode == wx.WXK_ESCAPE and modifiers == wx.MOD_NONE: + self.unselectAll() + elif keycode == 65 and modifiers == wx.MOD_CONTROL: + self.selectAll() + elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and modifiers == wx.MOD_NONE: + carts = self.getSelectedCarts() + self.removeCarts(carts) + event.Skip() + + def swapModule(self, x, y, modIdx): + """Swap a module from fitting window with carts""" + sFit = Fit.getInstance() + fit = sFit.getFit(self.mainFrame.getActiveFit()) + dstRow, _ = self.HitTest((x, y)) + + if dstRow > -1: + try: + dstCartsItemID = getattr(self.cart[dstRow], 'itemID', None) + except IndexError: + dstCartsItemID = None + else: + dstCartsItemID = None + + self.mainFrame.command.Submit(cmd.GuiLocalModuleToCartCommand( + fitID=self.mainFrame.getActiveFit(), + modPosition=modIdx, + cartItemID=dstCartsItemID, + copy=wx.GetMouseState().GetModifiers() == wx.MOD_CONTROL)) + + def fitChanged(self, event): + event.Skip() + activeFitID = self.mainFrame.getActiveFit() + if activeFitID is not None and activeFitID not in event.fitIDs: + return + + sFit = Fit.getInstance() + fit = sFit.getFit(activeFitID) + + # self.Parent.Parent.DisablePage(self, not fit or fit.isStructure) + + # Clear list and get out if current fitId is None + if activeFitID is None and self.lastFitId is not None: + self.DeleteAllItems() + self.lastFitId = None + return + + self.original = fit.cart if fit is not None else None + self.cart = fit.cart[:] if fit is not None else None + if self.cart is not None: + self.cart.sort(key=lambda c: (c.item.group.category.name, c.item.group.name, c.item.name)) + + if activeFitID != self.lastFitId: + self.lastFitId = activeFitID + + item = self.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_DONTCARE) + + if item != -1: + self.EnsureVisible(item) + + self.unselectAll() + + self.populate(self.cart) + self.refresh(self.cart) + + def onLeftDoubleClick(self, event): + row, _ = self.HitTest(event.Position) + if row != -1: + try: + cart = self.cart[row] + except IndexError: + return + self.removeCarts([cart]) + + def removeCarts(self, carts): + fitID = self.mainFrame.getActiveFit() + itemIDs = [] + for cart in carts: + if cart in self.original: + itemIDs.append(cart.itemID) + self.mainFrame.command.Submit(cmd.GuiRemoveCartCommand(fitID=fitID, itemIDs=itemIDs)) + + def spawnMenu(self, event): + clickedPos = self.getRowByAbs(event.Position) + self.ensureSelection(clickedPos) + + selection = self.getSelectedCarts() + mainCart = None + if clickedPos != -1: + try: + cart = self.cart[clickedPos] + except IndexError: + pass + else: + if cart in self.original: + mainCart = cart + itemContext = None if mainCart is None else Market.getInstance().getCategoryByItem( + mainCart.item).displayName + menu = ContextMenu.getMenu(self, mainCart, selection, ("cartItem", itemContext), + ("cartItemMisc", itemContext)) + if menu: + self.PopupMenu(menu) + + def getSelectedCarts(self): + carts = [] + for row in self.getSelectedRows(): + try: + cart = self.cart[row] + except IndexError: + continue + carts.append(cart) + return carts + + def getTabExtraText(self): + fitID = self.mainFrame.getActiveFit() + if fitID is None: + return None + sFit = Fit.getInstance() + fit = sFit.getFit(fitID) + if fit is None: + return None + opt = sFit.serviceFittingOptions["additionsLabels"] + # Total amount of cart items + if opt in (1, 2): + amount = len(fit.cart) + return ' ({})'.format(amount) if amount else None + else: + return None diff --git a/gui/builtinContextMenus/additionsImport.py b/gui/builtinContextMenus/additionsImport.py index bafda6a59c..1b5e753982 100644 --- a/gui/builtinContextMenus/additionsImport.py +++ b/gui/builtinContextMenus/additionsImport.py @@ -21,7 +21,8 @@ def __init__(self): 'cargoItemMisc': (_t('Cargo Items'), lambda i: not i.isAbyssal, cmd.GuiImportCargosCommand), 'implantItemMisc': (_t('Implants'), lambda i: i.isImplant, cmd.GuiImportImplantsCommand), 'implantItemMiscChar': (_t('Implants'), lambda i: i.isImplant, cmd.GuiImportImplantsCommand), - 'boosterItemMisc': (_t('Boosters'), lambda i: i.isBooster, cmd.GuiImportBoostersCommand) + 'boosterItemMisc': (_t('Boosters'), lambda i: i.isBooster, cmd.GuiImportBoostersCommand), + 'cartItemMisc':(_t('Cart'), lambda i: i.isCart, cmd.GuiImportCartCommand) } def display(self, callingWindow, srcContext): diff --git a/gui/builtinContextMenus/cartAdd.py b/gui/builtinContextMenus/cartAdd.py new file mode 100644 index 0000000000..0ca05a0146 --- /dev/null +++ b/gui/builtinContextMenus/cartAdd.py @@ -0,0 +1,43 @@ +import wx + +import gui.fitCommands as cmd +import gui.mainFrame +from gui.contextMenu import ContextMenuSingle +from service.fit import Fit + +_t = wx.GetTranslation + + +class AddToCart(ContextMenuSingle): + + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + def display(self, callingWindow, srcContext, mainItem): + if srcContext not in ("marketItemGroup", "marketItemMisc"): + return False + + if mainItem is None: + return False + + sFit = Fit.getInstance() + fitID = self.mainFrame.getActiveFit() + fit = sFit.getFit(fitID) + + if not fit or (fit.isStructure and mainItem.category.ID != 8): + return False + + return True + + def getText(self, callingWindow, itmContext, mainItem): + return _t("Add {} to Cart").format(itmContext) + + def activate(self, callingWindow, fullContext, mainItem, i): + fitID = self.mainFrame.getActiveFit() + typeID = int(mainItem.ID) + command = cmd.GuiAddCartCommand(fitID=fitID, itemID=typeID, amount=1) + if self.mainFrame.command.Submit(command): + self.mainFrame.additionsPane.select("Cart", focus=False) + + +AddToCart.register() diff --git a/gui/builtinContextMenus/cartAddAmmo.py b/gui/builtinContextMenus/cartAddAmmo.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gui/fitCommands/__init__.py b/gui/fitCommands/__init__.py index ab56b1ddcd..de46813dd0 100644 --- a/gui/fitCommands/__init__.py +++ b/gui/fitCommands/__init__.py @@ -9,6 +9,19 @@ from .gui.cargo.changeMetas import GuiChangeCargoMetasCommand from .gui.cargo.imprt import GuiImportCargosCommand from .gui.cargo.remove import GuiRemoveCargosCommand + +from .gui.cart.add import GuiAddCartCommand +from .gui.cart.changeAmount import GuiChangeCartsAmountCommand +from .gui.cart.changeMetas import GuiChangeCartMetasCommand +from .gui.cart.imprt import GuiImportCartCommand +from .gui.cart.remove import GuiRemoveCartCommand + + + + + + + from .gui.commandFit.add import GuiAddCommandFitsCommand from .gui.commandFit.remove import GuiRemoveCommandFitsCommand from .gui.commandFit.toggleStates import GuiToggleCommandFitStatesCommand @@ -60,6 +73,13 @@ from .gui.localModule.swap import GuiSwapLocalModulesCommand from .gui.localModuleCargo.cargoToLocalModule import GuiCargoToLocalModuleCommand from .gui.localModuleCargo.localModuleToCargo import GuiLocalModuleToCargoCommand + + +from .gui.localModuleCart.cartToLocalModule import GuiCartToLocalModuleCommand +from .gui.localModuleCart.localModuleToCart import GuiLocalModuleToCartCommand + + + from .gui.projectedChangeProjectionRange import GuiChangeProjectedItemsProjectionRangeCommand from .gui.projectedChangeStates import GuiChangeProjectedItemStatesCommand from .gui.projectedDrone.add import GuiAddProjectedDroneCommand diff --git a/gui/fitCommands/calc/cart/__init__.py b/gui/fitCommands/calc/cart/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gui/fitCommands/calc/cart/add.py b/gui/fitCommands/calc/cart/add.py new file mode 100644 index 0000000000..c2b75c6064 --- /dev/null +++ b/gui/fitCommands/calc/cart/add.py @@ -0,0 +1,35 @@ +import wx +from logbook import Logger + +from service.fit import Fit + + +pyfalog = Logger(__name__) + + +class CalcAddCartCommand(wx.Command): + + def __init__(self, fitID, cartInfo): + wx.Command.__init__(self, True, 'Add Cart') + self.fitID = fitID + self.cartInfo = cartInfo + + def Do(self): + pyfalog.debug('Doing addition of cart {} to fit {}'.format(self.cartInfo, self.fitID)) + fit = Fit.getInstance().getFit(self.fitID) + cart = next((c for c in fit.cart if c.itemID == self.cartInfo.itemID), None) + if cart is not None: + cart.amount += self.cartInfo.amount + else: + cart = self.cartInfo.toCart() + fit.cart.append(cart) + if cart not in fit.cart: + pyfalog.warning('Failed to append to list') + return False + return True + + def Undo(self): + pyfalog.debug('Undoing addition of cart {} to fit {}'.format(self.cartInfo, self.fitID)) + from .remove import CalcRemoveCartCommand + cmd = CalcRemoveCartCommand(fitID=self.fitID, cartInfo=self.cartInfo) + return cmd.Do() diff --git a/gui/fitCommands/calc/cart/changeAmount.py b/gui/fitCommands/calc/cart/changeAmount.py new file mode 100644 index 0000000000..ecc4799b57 --- /dev/null +++ b/gui/fitCommands/calc/cart/changeAmount.py @@ -0,0 +1,35 @@ +import wx +from logbook import Logger + +from gui.fitCommands.helpers import CartInfo +from service.fit import Fit + + +pyfalog = Logger(__name__) + + +class CalcChangeCartAmountCommand(wx.Command): + + def __init__(self, fitID, cartInfo): + wx.Command.__init__(self, True, 'Change Cart Amount') + self.fitID = fitID + self.cartInfo = cartInfo + self.savedCartInfo = None + + def Do(self): + pyfalog.debug('Doing change of cart {} for fit {}'.format(self.cartInfo, self.fitID)) + fit = Fit.getInstance().getFit(self.fitID) + cart = next((c for c in fit.cart if c.itemID == self.cartInfo.itemID), None) + if cart is None: + pyfalog.warning('Cannot find cart item') + return False + self.savedCartInfo = CartInfo.fromCart(cart) + if self.cartInfo.amount == self.savedCartInfo.amount: + return False + cart.amount = self.cartInfo.amount + return True + + def Undo(self): + pyfalog.debug('Undoing change of cart {} for fit {}'.format(self.cartInfo, self.fitID)) + cmd = CalcChangeCartAmountCommand(fitID=self.fitID, cartInfo=self.savedCartInfo) + return cmd.Do() diff --git a/gui/fitCommands/calc/cart/remove.py b/gui/fitCommands/calc/cart/remove.py new file mode 100644 index 0000000000..65dc1ce5c9 --- /dev/null +++ b/gui/fitCommands/calc/cart/remove.py @@ -0,0 +1,37 @@ +import wx +from logbook import Logger + +from gui.fitCommands.helpers import CartInfo +from service.fit import Fit + + +pyfalog = Logger(__name__) + + +class CalcRemoveCartCommand(wx.Command): + + def __init__(self, fitID, cartInfo): + wx.Command.__init__(self, True, 'Remove Cart') + self.fitID = fitID + self.cartInfo = cartInfo + self.savedRemovedAmount = None + + def Do(self): + pyfalog.debug('Doing removal of cart {} to fit {}'.format(self.cartInfo, self.fitID)) + fit = Fit.getInstance().getFit(self.fitID) + cart = next((x for x in fit.cart if x.itemID == self.cartInfo.itemID), None) + if cart is None: + return False + self.savedRemovedAmount = min(cart.amount, self.cartInfo.amount) + cart.amount -= self.savedRemovedAmount + if cart.amount <= 0: + fit.cart.remove(cart) + return True + + def Undo(self): + pyfalog.debug('Undoing removal of cart {} to fit {}'.format(self.cartInfo, self.fitID)) + from .add import CalcAddCartCommand + cmd = CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=self.cartInfo.itemID, amount=self.savedRemovedAmount)) + return cmd.Do() diff --git a/gui/fitCommands/gui/__init__.py b/gui/fitCommands/gui/__init__.py index e69de29bb2..8b13789179 100644 --- a/gui/fitCommands/gui/__init__.py +++ b/gui/fitCommands/gui/__init__.py @@ -0,0 +1 @@ + diff --git a/gui/fitCommands/gui/cart/__init__.py b/gui/fitCommands/gui/cart/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gui/fitCommands/gui/cart/add.py b/gui/fitCommands/gui/cart/add.py new file mode 100644 index 0000000000..1946713f59 --- /dev/null +++ b/gui/fitCommands/gui/cart/add.py @@ -0,0 +1,32 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.cart.add import CalcAddCartCommand +from gui.fitCommands.helpers import CargoInfo, InternalCommandHistory +from service.market import Market + + +class GuiAddCartCommand(wx.Command): + + def __init__(self, fitID, itemID, amount): + wx.Command.__init__(self, True, 'Add Cart') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.itemID = itemID + self.amount = amount + + def Do(self): + cmd = CalcAddCartCommand(fitID=self.fitID, cartInfo=CargoInfo(itemID=self.itemID, amount=self.amount)) + success = self.internalHistory.submit(cmd) + Market.getInstance().storeRecentlyUsed(self.itemID) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success + + def Undo(self): + success = self.internalHistory.undoAll() + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success diff --git a/gui/fitCommands/gui/cart/changeAmount.py b/gui/fitCommands/gui/cart/changeAmount.py new file mode 100644 index 0000000000..5afc197f92 --- /dev/null +++ b/gui/fitCommands/gui/cart/changeAmount.py @@ -0,0 +1,41 @@ +import math + +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.cart.changeAmount import CalcChangeCartAmountCommand +from gui.fitCommands.calc.cart.remove import CalcRemoveCartCommand +from gui.fitCommands.helpers import CartInfo, InternalCommandHistory + + +class GuiChangeCartsAmountCommand(wx.Command): + + def __init__(self, fitID, itemIDs, amount): + wx.Command.__init__(self, True, 'Change Cargo Amount') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.itemIDs = itemIDs + self.amount = amount + + def Do(self): + results = [] + if self.amount > 0: + for itemID in self.itemIDs: + cmd = CalcChangeCartAmountCommand(fitID=self.fitID, cartInfo=CartInfo(itemID=itemID, amount=self.amount)) + results.append(self.internalHistory.submit(cmd)) + else: + for itemID in self.itemIDs: + cmd = CalcRemoveCartCommand(fitID=self.fitID, cartInfo=CartInfo(itemID=itemID, amount=math.inf)) + results.append(self.internalHistory.submit(cmd)) + success = any(results) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success + + def Undo(self): + success = self.internalHistory.undoAll() + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success diff --git a/gui/fitCommands/gui/cart/changeMetas.py b/gui/fitCommands/gui/cart/changeMetas.py new file mode 100644 index 0000000000..cdc039d602 --- /dev/null +++ b/gui/fitCommands/gui/cart/changeMetas.py @@ -0,0 +1,49 @@ +import math +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.cart.add import CalcAddCartCommand +from gui.fitCommands.calc.cart.remove import CalcRemoveCartCommand +from gui.fitCommands.helpers import CartInfo, InternalCommandHistory +from service.fit import Fit + + +class GuiChangeCartMetasCommand(wx.Command): + + def __init__(self, fitID, itemIDs, newItemID): + wx.Command.__init__(self, True, 'Change Cart Metas') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.itemIDs = itemIDs + self.newItemID = newItemID + + def Do(self): + sFit = Fit.getInstance() + fit = sFit.getFit(self.fitID) + results = [] + for itemID in self.itemIDs: + if itemID == self.newItemID: + continue + cargo = next((c for c in fit.cargo if c.itemID == itemID), None) + if cargo is None: + continue + amount = cargo.amount + cmdRemove = CalcRemoveCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=itemID, amount=math.inf)) + cmdAdd = CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=self.newItemID, amount=amount)) + results.append(self.internalHistory.submitBatch(cmdRemove, cmdAdd)) + success = any(results) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success + + def Undo(self): + success = self.internalHistory.undoAll() + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success diff --git a/gui/fitCommands/gui/cart/imprt.py b/gui/fitCommands/gui/cart/imprt.py new file mode 100644 index 0000000000..559562eb7e --- /dev/null +++ b/gui/fitCommands/gui/cart/imprt.py @@ -0,0 +1,36 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.cart.add import CalcAddCartCommand +from gui.fitCommands.helpers import CartInfo, InternalCommandHistory + + +class GuiImportCartCommand(wx.Command): + + def __init__(self, fitID, cargos): + wx.Command.__init__(self, True, 'Import Carts') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.cargos = {} + for itemID, amount, mutation in cargos: + if itemID not in self.cargos: + self.cargos[itemID] = 0 + self.cargos[itemID] += amount + + def Do(self): + results = [] + for itemID, amount in self.cargos.items(): + cmd = CalcAddCartCommand(fitID=self.fitID, cartInfo=CartInfo(itemID=itemID, amount=amount)) + results.append(self.internalHistory.submit(cmd)) + success = any(results) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success + + def Undo(self): + success = self.internalHistory.undoAll() + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success diff --git a/gui/fitCommands/gui/cart/remove.py b/gui/fitCommands/gui/cart/remove.py new file mode 100644 index 0000000000..968f4efc3b --- /dev/null +++ b/gui/fitCommands/gui/cart/remove.py @@ -0,0 +1,39 @@ +import math + +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.cart.remove import CalcRemoveCartCommand +from gui.fitCommands.helpers import CartInfo, InternalCommandHistory +from service.market import Market + + +class GuiRemoveCartCommand(wx.Command): + + def __init__(self, fitID, itemIDs): + wx.Command.__init__(self, True, 'Remove Carts') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.itemIDs = itemIDs + + def Do(self): + sMkt = Market.getInstance() + results = [] + for itemID in self.itemIDs: + cmd = CalcRemoveCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=itemID, amount=math.inf)) + results.append(self.internalHistory.submit(cmd)) + sMkt.storeRecentlyUsed(itemID) + success = any(results) + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success + + def Undo(self): + success = self.internalHistory.undoAll() + eos.db.commit() + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), GE.FitChanged(fitIDs=(self.fitID,))) + return success diff --git a/gui/fitCommands/gui/helpers.py b/gui/fitCommands/gui/helpers.py new file mode 100644 index 0000000000..133aad11fb --- /dev/null +++ b/gui/fitCommands/gui/helpers.py @@ -0,0 +1,591 @@ +import math + +import wx +from logbook import Logger + +import eos.db +from eos.const import FittingModuleState +from eos.saveddata.booster import Booster +from eos.saveddata.cargo import Cargo + +from eos.saveddata.cart import Cart + +from eos.saveddata.drone import Drone +from eos.saveddata.fighter import Fighter +from eos.saveddata.implant import Implant +from eos.saveddata.module import Module +from service.market import Market +from utils.repr import makeReprStr + + +pyfalog = Logger(__name__) + + +class InternalCommandHistory: + + def __init__(self): + self.__buffer = wx.CommandProcessor() + + def submit(self, command): + return self.__buffer.Submit(command) + + def submitBatch(self, *commands): + for command in commands: + if not self.__buffer.Submit(command): + # Undo what we already submitted + for commandToUndo in reversed(self.__buffer.Commands): + if commandToUndo in commands: + self.__buffer.Undo() + return False + return True + + def undoAll(self): + undoneCommands = [] + # Undo commands one by one, starting from the last + for commandToUndo in reversed(self.__buffer.Commands): + if commandToUndo.Undo(): + undoneCommands.append(commandToUndo) + # If undoing fails, redo already undone commands, starting from the last undone + else: + for commandToRedo in reversed(undoneCommands): + if not commandToRedo.Do(): + break + self.__buffer.ClearCommands() + return False + self.__buffer.ClearCommands() + return True + + def __len__(self): + return len(self.__buffer.Commands) + + +class ModuleInfo: + + def __init__( + self, itemID, baseItemID=None, mutaplasmidID=None, mutations=None, chargeID=None, + state=None, spoolType=None, spoolAmount=None, rahPattern=None): + self.itemID = itemID + self.baseItemID = baseItemID + self.mutaplasmidID = mutaplasmidID + self.mutations = mutations + self.chargeID = chargeID + self.state = state + self.spoolType = spoolType + self.spoolAmount = spoolAmount + self.rahPattern = rahPattern + + @classmethod + def fromModule(cls, mod, unmutate=False): + if mod is None: + return None + if unmutate and mod.isMutated: + info = cls( + itemID=mod.baseItemID, + baseItemID=None, + mutaplasmidID=None, + mutations={}, + chargeID=mod.chargeID, + state=mod.state, + spoolType=mod.spoolType, + spoolAmount=mod.spoolAmount, + rahPattern=mod.rahPatternOverride) + else: + info = cls( + itemID=mod.itemID, + baseItemID=mod.baseItemID, + mutaplasmidID=mod.mutaplasmidID, + mutations={m.attrID: m.value for m in mod.mutators.values()}, + chargeID=mod.chargeID, + state=mod.state, + spoolType=mod.spoolType, + spoolAmount=mod.spoolAmount, + rahPattern=mod.rahPatternOverride) + return info + + def toModule(self, fallbackState=None): + mkt = Market.getInstance() + + item = mkt.getItem(self.itemID, eager=('attributes', 'group.category')) + if self.baseItemID and self.mutaplasmidID: + baseItem = mkt.getItem(self.baseItemID, eager=('attributes', 'group.category')) + mutaplasmid = eos.db.getDynamicItem(self.mutaplasmidID) + else: + baseItem = None + mutaplasmid = None + try: + mod = Module(item, baseItem=baseItem, mutaplasmid=mutaplasmid) + except ValueError: + pyfalog.warning('Invalid item: {}'.format(self.itemID)) + return None + + if self.mutations is not None: + for attrID, mutator in mod.mutators.items(): + if attrID in self.mutations: + mutator.value = self.mutations[attrID] + + if self.spoolType is not None and self.spoolAmount is not None: + mod.spoolType = self.spoolType + mod.spoolAmount = self.spoolAmount + + mod.rahPatternOverride = self.rahPattern + + if self.state is not None: + if mod.isValidState(self.state): + mod.state = self.state + else: + mod.state = mod.getMaxState(proposedState=self.state) + elif fallbackState is not None: + if mod.isValidState(fallbackState): + mod.state = fallbackState + + if self.chargeID is not None: + charge = mkt.getItem(self.chargeID, eager=('attributes',)) + if charge is None: + pyfalog.warning('Cannot set charge {}'.format(self.chargeID)) + return None + mod.charge = charge + + return mod + + def __eq__(self, other): + if not isinstance(other, ModuleInfo): + return False + return all(( + self.itemID == other.itemID, + self.baseItemID == other.baseItemID, + self.mutaplasmidID == other.mutaplasmidID, + self.mutations == other.mutations, + self.chargeID == other.chargeID, + self.state == other.state, + self.spoolType == other.spoolType, + self.spoolAmount == other.spoolAmount, + self.rahPattern == other.rahPattern)) + + def __repr__(self): + return makeReprStr(self, [ + 'itemID', 'baseItemID', 'mutaplasmidID', 'mutations', + 'chargeID', 'state', 'spoolType', 'spoolAmount', 'rahPattern']) + + +class DroneInfo: + + def __init__(self, amount, amountActive, itemID, baseItemID=None, mutaplasmidID=None, mutations=None): + self.itemID = itemID + self.baseItemID = baseItemID + self.mutaplasmidID = mutaplasmidID + self.mutations = mutations + self.amount = amount + self.amountActive = amountActive + + @classmethod + def fromDrone(cls, drone): + if drone is None: + return None + info = cls( + itemID=drone.itemID, + amount=drone.amount, + amountActive=drone.amountActive, + baseItemID=drone.baseItemID, + mutaplasmidID=drone.mutaplasmidID, + mutations={m.attrID: m.value for m in drone.mutators.values()}) + return info + + def toDrone(self): + mkt = Market.getInstance() + item = mkt.getItem(self.itemID, eager=('attributes', 'group.category')) + if self.baseItemID and self.mutaplasmidID: + baseItem = mkt.getItem(self.baseItemID, eager=('attributes', 'group.category')) + mutaplasmid = eos.db.getDynamicItem(self.mutaplasmidID) + else: + baseItem = None + mutaplasmid = None + try: + drone = Drone(item, baseItem=baseItem, mutaplasmid=mutaplasmid) + except ValueError: + pyfalog.warning('Invalid item: {}'.format(self.itemID)) + return None + + if self.mutations is not None: + for attrID, mutator in drone.mutators.items(): + if attrID in self.mutations: + mutator.value = self.mutations[attrID] + + drone.amount = self.amount + drone.amountActive = self.amountActive + return drone + + def __repr__(self): + return makeReprStr(self, [ + 'itemID', 'amount', 'amountActive', + 'baseItemID', 'mutaplasmidID', 'mutations']) + + +class FighterInfo: + + def __init__(self, itemID, amount=None, state=None, abilities=None): + self.itemID = itemID + self.amount = amount + self.state = state + self.abilities = abilities + + @classmethod + def fromFighter(cls, fighter): + if fighter is None: + return None + info = cls( + itemID=fighter.itemID, + amount=fighter.amount, + state=fighter.active, + abilities={fa.effectID: fa.active for fa in fighter.abilities}) + return info + + def toFighter(self): + item = Market.getInstance().getItem(self.itemID, eager=('attributes', 'group.category')) + try: + fighter = Fighter(item) + except ValueError: + pyfalog.warning('Invalid item: {}'.format(self.itemID)) + return None + if self.amount is not None: + fighter.amount = self.amount + if self.state is not None: + fighter.active = self.state + if self.abilities is not None: + for ability in fighter.abilities: + ability.active = self.abilities.get(ability.effectID, ability.active) + return fighter + + def __repr__(self): + return makeReprStr(self, ['itemID', 'amount', 'state', 'abilities']) + + +class ImplantInfo: + + def __init__(self, itemID, state=None): + self.itemID = itemID + self.state = state + + @classmethod + def fromImplant(cls, implant): + if implant is None: + return None + info = cls( + itemID=implant.itemID, + state=implant.active) + return info + + def toImplant(self): + item = Market.getInstance().getItem(self.itemID, eager=('attributes', 'group.category')) + try: + implant = Implant(item) + except ValueError: + pyfalog.warning('Invalid item: {}'.format(self.itemID)) + return None + if self.state is not None: + implant.active = self.state + return implant + + def __repr__(self): + return makeReprStr(self, ['itemID', 'state']) + + +class BoosterInfo: + + def __init__(self, itemID, state=None, sideEffects=None): + self.itemID = itemID + self.state = state + self.sideEffects = sideEffects + + @classmethod + def fromBooster(cls, booster): + if booster is None: + return None + info = cls( + itemID=booster.itemID, + state=booster.active, + sideEffects={se.effectID: se.active for se in booster.sideEffects}) + return info + + def toBooster(self): + item = Market.getInstance().getItem(self.itemID, eager=('attributes', 'group.category')) + try: + booster = Booster(item) + except ValueError: + pyfalog.warning('Invalid item: {}'.format(self.itemID)) + return None + if self.state is not None: + booster.active = self.state + if self.sideEffects is not None: + for sideEffect in booster.sideEffects: + sideEffect.active = self.sideEffects.get(sideEffect.effectID, sideEffect.active) + return booster + + def __repr__(self): + return makeReprStr(self, ['itemID', 'state', 'sideEffects']) + + +class CargoInfo: + + def __init__(self, itemID, amount): + self.itemID = itemID + self.amount = amount + + @classmethod + def fromCargo(cls, cargo): + if cargo is None: + return None + info = cls( + itemID=cargo.itemID, + amount=cargo.amount) + return info + + def toCargo(self): + item = Market.getInstance().getItem(self.itemID) + cargo = Cargo(item) + cargo.amount = self.amount + return cargo + + def __repr__(self): + return makeReprStr(self, ['itemID', 'amount']) + + +def activeStateLimit(itemIdentity): + item = Market.getInstance().getItem(itemIdentity) + if { + 'moduleBonusAssaultDamageControl', 'moduleBonusIndustrialInvulnerability', + 'microJumpDrive', 'microJumpPortalDrive', 'emergencyHullEnergizer', + 'cynosuralGeneration', 'jumpPortalGeneration', 'jumpPortalGenerationBO', + 'cloneJumpAccepting', 'cloakingWarpSafe', 'cloakingPrototype', 'cloaking', + 'massEntanglerEffect5', 'electronicAttributeModifyOnline', 'targetPassively', + 'cargoScan', 'shipScan', 'surveyScan', 'targetSpectrumBreakerBonus', + 'interdictionNullifierBonus', 'warpCoreStabilizerActive', + 'industrialItemCompression' + }.intersection(item.effects): + return FittingModuleState.ONLINE + return FittingModuleState.ACTIVE + + +def droneStackLimit(fit, itemIdentity): + item = Market.getInstance().getItem(itemIdentity) + hardLimit = max(5, fit.extraAttributes["maxActiveDrones"]) + releaseLimit = fit.getReleaseLimitForDrone(item) + limit = min(hardLimit, releaseLimit if releaseLimit > 0 else math.inf) + return limit + + +def restoreCheckedStates(fit, stateInfo, ignoreModPoss=()): + if stateInfo is None: + return + changedMods, changedProjMods, changedProjDrones = stateInfo + for pos, state in changedMods.items(): + if pos in ignoreModPoss: + continue + fit.modules[pos].state = state + for pos, state in changedProjMods.items(): + fit.projectedModules[pos].state = state + for pos, amountActive in changedProjDrones.items(): + fit.projectedDrones[pos].amountActive = amountActive + + +def restoreRemovedDummies(fit, dummyInfo): + if dummyInfo is None: + return + # Need this to properly undo the case when removal of subsystems removes dummy slots + for position in sorted(dummyInfo): + slot = dummyInfo[position] + fit.modules.insert(position, Module.buildEmpty(slot)) + + +def getSimilarModPositions(mods, mainMod): + sMkt = Market.getInstance() + mainGroupID = getattr(sMkt.getGroupByItem(mainMod.item), 'ID', None) + mainMktGroupID = getattr(sMkt.getMarketGroupByItem(mainMod.item), 'ID', None) + mainEffects = set(getattr(mainMod.item, 'effects', ())) + positions = [] + for position, mod in enumerate(mods): + if mod.isEmpty: + continue + # Always include selected module itself + if mod is mainMod: + positions.append(position) + continue + if mod.itemID is None: + continue + # Modules which have the same item ID + if mod.itemID == mainMod.itemID: + positions.append(position) + continue + # And modules from the same group and market group too + modGroupID = getattr(sMkt.getGroupByItem(mod.item), 'ID', None) + modMktGroupID = getattr(sMkt.getMarketGroupByItem(mod.item), 'ID', None) + modEffects = set(getattr(mod.item, 'effects', ())) + if ( + modGroupID is not None and modGroupID == mainGroupID and + modMktGroupID is not None and modMktGroupID == mainMktGroupID and + modEffects == mainEffects + ): + positions.append(position) + continue + return positions + + +def getSimilarFighters(fighters, mainFighter): + sMkt = Market.getInstance() + mainGroupID = getattr(sMkt.getGroupByItem(mainFighter.item), 'ID', None) + mainAbilityIDs = set(a.effectID for a in mainFighter.abilities) + similarFighters = [] + for fighter in fighters: + # Always include selected fighter itself + if fighter is mainFighter: + similarFighters.append(fighter) + continue + if fighter.itemID is None: + continue + # Fighters which have the same item ID + if fighter.itemID == mainFighter.itemID: + similarFighters.append(fighter) + continue + # And fighters from the same group and with the same abilities too + fighterGroupID = getattr(sMkt.getGroupByItem(fighter.item), 'ID', None) + fighterAbilityIDs = set(a.effectID for a in fighter.abilities) + if ( + fighterGroupID is not None and fighterGroupID == mainGroupID and + len(fighterAbilityIDs) > 0 and fighterAbilityIDs == mainAbilityIDs + ): + similarFighters.append(fighter) + continue + return similarFighters + + +class CartInfo: + + def __init__(self, itemID, amount): + self.itemID = itemID + self.amount = amount + + @classmethod + def fromCart(cls, cart): + if cart is None: + return None + info = cls( + itemID=cart.itemID, + amount=cart.amount) + return info + + def toCart(self): + item = Market.getInstance().getItem(self.itemID) + cart = Cart(item) + cart.amount = self.amount + return cart + + def __repr__(self): + return makeReprStr(self, ['itemID', 'amount']) + + +def activeStateLimit(itemIdentity): + item = Market.getInstance().getItem(itemIdentity) + if { + 'moduleBonusAssaultDamageControl', 'moduleBonusIndustrialInvulnerability', + 'microJumpDrive', 'microJumpPortalDrive', 'emergencyHullEnergizer', + 'cynosuralGeneration', 'jumpPortalGeneration', 'jumpPortalGenerationBO', + 'cloneJumpAccepting', 'cloakingWarpSafe', 'cloakingPrototype', 'cloaking', + 'massEntanglerEffect5', 'electronicAttributeModifyOnline', 'targetPassively', + 'cartScan', 'shipScan', 'surveyScan', 'targetSpectrumBreakerBonus', + 'interdictionNullifierBonus', 'warpCoreStabilizerActive', + 'industrialItemCompression' + }.intersection(item.effects): + return FittingModuleState.ONLINE + return FittingModuleState.ACTIVE + + +def droneStackLimit(fit, itemIdentity): + item = Market.getInstance().getItem(itemIdentity) + hardLimit = max(5, fit.extraAttributes["maxActiveDrones"]) + releaseLimit = fit.getReleaseLimitForDrone(item) + limit = min(hardLimit, releaseLimit if releaseLimit > 0 else math.inf) + return limit + + +def restoreCheckedStates(fit, stateInfo, ignoreModPoss=()): + if stateInfo is None: + return + changedMods, changedProjMods, changedProjDrones = stateInfo + for pos, state in changedMods.items(): + if pos in ignoreModPoss: + continue + fit.modules[pos].state = state + for pos, state in changedProjMods.items(): + fit.projectedModules[pos].state = state + for pos, amountActive in changedProjDrones.items(): + fit.projectedDrones[pos].amountActive = amountActive + + +def restoreRemovedDummies(fit, dummyInfo): + if dummyInfo is None: + return + # Need this to properly undo the case when removal of subsystems removes dummy slots + for position in sorted(dummyInfo): + slot = dummyInfo[position] + fit.modules.insert(position, Module.buildEmpty(slot)) + + +def getSimilarModPositions(mods, mainMod): + sMkt = Market.getInstance() + mainGroupID = getattr(sMkt.getGroupByItem(mainMod.item), 'ID', None) + mainMktGroupID = getattr(sMkt.getMarketGroupByItem(mainMod.item), 'ID', None) + mainEffects = set(getattr(mainMod.item, 'effects', ())) + positions = [] + for position, mod in enumerate(mods): + if mod.isEmpty: + continue + # Always include selected module itself + if mod is mainMod: + positions.append(position) + continue + if mod.itemID is None: + continue + # Modules which have the same item ID + if mod.itemID == mainMod.itemID: + positions.append(position) + continue + # And modules from the same group and market group too + modGroupID = getattr(sMkt.getGroupByItem(mod.item), 'ID', None) + modMktGroupID = getattr(sMkt.getMarketGroupByItem(mod.item), 'ID', None) + modEffects = set(getattr(mod.item, 'effects', ())) + if ( + modGroupID is not None and modGroupID == mainGroupID and + modMktGroupID is not None and modMktGroupID == mainMktGroupID and + modEffects == mainEffects + ): + positions.append(position) + continue + return positions + + +def getSimilarFighters(fighters, mainFighter): + sMkt = Market.getInstance() + mainGroupID = getattr(sMkt.getGroupByItem(mainFighter.item), 'ID', None) + mainAbilityIDs = set(a.effectID for a in mainFighter.abilities) + similarFighters = [] + for fighter in fighters: + # Always include selected fighter itself + if fighter is mainFighter: + similarFighters.append(fighter) + continue + if fighter.itemID is None: + continue + # Fighters which have the same item ID + if fighter.itemID == mainFighter.itemID: + similarFighters.append(fighter) + continue + # And fighters from the same group and with the same abilities too + fighterGroupID = getattr(sMkt.getGroupByItem(fighter.item), 'ID', None) + fighterAbilityIDs = set(a.effectID for a in fighter.abilities) + if ( + fighterGroupID is not None and fighterGroupID == mainGroupID and + len(fighterAbilityIDs) > 0 and fighterAbilityIDs == mainAbilityIDs + ): + similarFighters.append(fighter) + continue + return similarFighters + diff --git a/gui/fitCommands/gui/localModuleCart/__init__.py b/gui/fitCommands/gui/localModuleCart/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gui/fitCommands/gui/localModuleCart/cartToLocalModule.py b/gui/fitCommands/gui/localModuleCart/cartToLocalModule.py new file mode 100644 index 0000000000..5361ad728d --- /dev/null +++ b/gui/fitCommands/gui/localModuleCart/cartToLocalModule.py @@ -0,0 +1,165 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.cart.add import CalcAddCartCommand +from gui.fitCommands.calc.cart.remove import CalcRemoveCartCommand +from gui.fitCommands.calc.module.changeCharges import CalcChangeModuleChargesCommand +from gui.fitCommands.calc.module.localReplace import CalcReplaceLocalModuleCommand +from gui.fitCommands.helpers import CartInfo, InternalCommandHistory, ModuleInfo, restoreRemovedDummies +from service.fit import Fit + + +class GuiCartToLocalModuleCommand(wx.Command): + """ + Moves cart to the fitting window. If target is not empty, take whatever we take off and put + into the cart hold. If we copy, we do the same but do not remove the item from the cart hold. + """ + + def __init__(self, fitID, cartItemID, modPosition, copy): + wx.Command.__init__(self, True, 'Cart to Local Module') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.srcCartItemID = cartItemID + self.dstModPosition = modPosition + self.copy = copy + self.removedModItemID = None + self.addedModItemID = None + self.savedRemovedDummies = None + + def Do(self): + sFit = Fit.getInstance() + fit = sFit.getFit(self.fitID) + srcCart = next((c for c in fit.cart if c.itemID == self.srcCartItemID), None) + if srcCart is None: + return False + dstMod = fit.modules[self.dstModPosition] + # Moving/copying charge from cart to fit + if srcCart.item.isCharge and not dstMod.isEmpty: + newCartChargeItemID = dstMod.chargeID + newCartChargeAmount = dstMod.numCharges + newModChargeItemID = self.srcCartItemID + newModChargeAmount = dstMod.getNumCharges(srcCart.item) + if newCartChargeItemID == newModChargeItemID: + return False + commands = [] + if not self.copy: + commands.append(CalcRemoveCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=newModChargeItemID, amount=newModChargeAmount))) + if newCartChargeItemID is not None: + commands.append(CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=newCartChargeItemID, amount=newCartChargeAmount))) + commands.append(CalcChangeModuleChargesCommand( + fitID=self.fitID, + projected=False, + chargeMap={self.dstModPosition: self.srcCartItemID})) + success = self.internalHistory.submitBatch(*commands) + # Moving/copying/replacing module + elif srcCart.item.isModule: + dstModItemID = dstMod.itemID + dstModSlot = dstMod.slot + if self.srcCartItemID == dstModItemID: + return False + # To keep all old item properties, copy them over from old module, except for mutations + newModInfo = ModuleInfo.fromModule(dstMod, unmutate=True) + newModInfo.itemID = self.srcCartItemID + if dstMod.isEmpty: + newCartModItemID = None + dstModChargeItemID = None + dstModChargeAmount = None + else: + # We cannot put mutated items to cart, so use unmutated item ID + newCartModItemID = ModuleInfo.fromModule(dstMod, unmutate=True).itemID + dstModChargeItemID = dstMod.chargeID + dstModChargeAmount = dstMod.numCharges + commands = [] + # Keep cart only in case we were copying + if not self.copy: + commands.append(CalcRemoveCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=self.srcCartItemID, amount=1))) + # Add item to cart only if we copied/moved to non-empty slot + if newCartModItemID is not None: + commands.append(CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=newCartModItemID, amount=1))) + cmdReplace = CalcReplaceLocalModuleCommand( + fitID=self.fitID, + position=self.dstModPosition, + newModInfo=newModInfo, + unloadInvalidCharges=True) + commands.append(cmdReplace) + # Submit batch now because we need to have updated info on fit to keep going + success = self.internalHistory.submitBatch(*commands) + newMod = fit.modules[self.dstModPosition] + # Bail if drag happened to slot to which module cannot be dragged, will undo later + if newMod.slot != dstModSlot: + success = False + if success: + # If we had to unload charge, add it to cart + if cmdReplace.unloadedCharge and dstModChargeItemID is not None: + cmdAddCartCharge = CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=dstModChargeItemID, amount=dstModChargeAmount)) + success = self.internalHistory.submit(cmdAddCartCharge) + # If we did not unload charge and there still was a charge, see if amount differs and process it + elif not cmdReplace.unloadedCharge and dstModChargeItemID is not None: + # How many extra charges do we need to take from cart + extraChargeAmount = newMod.numCharges - dstModChargeAmount + if extraChargeAmount > 0: + cmdRemoveCartExtraCharge = CalcRemoveCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=dstModChargeItemID, amount=extraChargeAmount)) + # Do not check if operation was successful or not, we're okay if we have no such + # charges in cart + self.internalHistory.submit(cmdRemoveCartExtraCharge) + elif extraChargeAmount < 0: + cmdAddCartExtraCharge = CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=dstModChargeItemID, amount=abs(extraChargeAmount))) + success = self.internalHistory.submit(cmdAddCartExtraCharge) + if success: + # Store info to properly send events later + self.removedModItemID = dstModItemID + self.addedModItemID = self.srcCartItemID + else: + self.internalHistory.undoAll() + else: + return False + eos.db.flush() + sFit.recalc(self.fitID) + self.savedRemovedDummies = sFit.fill(self.fitID) + eos.db.commit() + events = [] + if self.removedModItemID is not None: + events.append(GE.FitChanged(fitIDs=(self.fitID,), action='moddel', typeID=self.removedModItemID)) + if self.addedModItemID is not None: + events.append(GE.FitChanged(fitIDs=(self.fitID,), action='modadd', typeID=self.addedModItemID)) + if not events: + events.append(GE.FitChanged(fitIDs=(self.fitID,))) + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + return success + + def Undo(self): + sFit = Fit.getInstance() + fit = sFit.getFit(self.fitID) + restoreRemovedDummies(fit, self.savedRemovedDummies) + success = self.internalHistory.undoAll() + eos.db.flush() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + events = [] + if self.addedModItemID is not None: + events.append(GE.FitChanged(fitIDs=(self.fitID,), action='moddel', typeID=self.addedModItemID)) + if self.removedModItemID is not None: + events.append(GE.FitChanged(fitIDs=(self.fitID,), action='modadd', typeID=self.removedModItemID)) + if not events: + events.append(GE.FitChanged(fitIDs=(self.fitID,))) + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + return success diff --git a/gui/fitCommands/gui/localModuleCart/localModuleToCart.py b/gui/fitCommands/gui/localModuleCart/localModuleToCart.py new file mode 100644 index 0000000000..964d96a5cb --- /dev/null +++ b/gui/fitCommands/gui/localModuleCart/localModuleToCart.py @@ -0,0 +1,145 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from gui.fitCommands.calc.cart.add import CalcAddCartCommand +from gui.fitCommands.calc.cart.remove import CalcRemoveCartCommand +from gui.fitCommands.calc.module.localRemove import CalcRemoveLocalModulesCommand +from gui.fitCommands.calc.module.localReplace import CalcReplaceLocalModuleCommand +from gui.fitCommands.helpers import CartInfo, InternalCommandHistory, ModuleInfo, restoreRemovedDummies +from service.fit import Fit + + +class GuiLocalModuleToCartCommand(wx.Command): + + def __init__(self, fitID, modPosition, cartItemID, copy): + wx.Command.__init__(self, True, 'Local Module to Cart') + self.internalHistory = InternalCommandHistory() + self.fitID = fitID + self.srcModPosition = modPosition + self.dstCartItemID = cartItemID + self.copy = copy + self.removedModItemID = None + self.addedModItemID = None + self.savedRemovedDummies = None + + def Do(self): + fit = Fit.getInstance().getFit(self.fitID) + srcMod = fit.modules[self.srcModPosition] + if srcMod.isEmpty: + return False + srcModItemID = srcMod.itemID + dstCart = next((c for c in fit.cart if c.itemID == self.dstCartItemID), None) + success = False + # Attempt to swap if we're moving our module onto a module in the cart hold + if not self.copy and dstCart is not None and dstCart.item.isModule: + if srcModItemID == self.dstCartItemID: + return False + srcModSlot = srcMod.slot + newModInfo = ModuleInfo.fromModule(srcMod, unmutate=True) + newModInfo.itemID = self.dstCartItemID + srcModChargeItemID = srcMod.chargeID + srcModChargeAmount = srcMod.numCharges + commands = [] + commands.append(CalcRemoveCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=self.dstCartItemID, amount=1))) + commands.append(CalcAddCartCommand( + fitID=self.fitID, + # We cannot put mutated items to cart, so use unmutated item ID + cartInfo=CartInfo(itemID=ModuleInfo.fromModule(srcMod, unmutate=True).itemID, amount=1))) + cmdReplace = CalcReplaceLocalModuleCommand( + fitID=self.fitID, + position=self.srcModPosition, + newModInfo=newModInfo, + unloadInvalidCharges=True) + commands.append(cmdReplace) + # Submit batch now because we need to have updated info on fit to keep going + success = self.internalHistory.submitBatch(*commands) + if success: + newMod = fit.modules[self.srcModPosition] + # Process charge changes if module is moved to proper slot + if newMod.slot == srcModSlot: + # If we had to unload charge, add it to cart + if cmdReplace.unloadedCharge and srcModChargeItemID is not None: + cmdAddCartCharge = CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=srcModChargeItemID, amount=srcModChargeAmount)) + success = self.internalHistory.submit(cmdAddCartCharge) + # If we did not unload charge and there still was a charge, see if amount differs and process it + elif not cmdReplace.unloadedCharge and srcModChargeItemID is not None: + # How many extra charges do we need to take from cart + extraChargeAmount = newMod.numCharges - srcModChargeAmount + if extraChargeAmount > 0: + cmdRemoveCartExtraCharge = CalcRemoveCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=srcModChargeItemID, amount=extraChargeAmount)) + # Do not check if operation was successful or not, we're okay if we have no such + # charges in cart + self.internalHistory.submit(cmdRemoveCartExtraCharge) + elif extraChargeAmount < 0: + cmdAddCartExtraCharge = CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=srcModChargeItemID, amount=abs(extraChargeAmount))) + success = self.internalHistory.submit(cmdAddCartExtraCharge) + if success: + # Store info to properly send events later + self.removedModItemID = srcModItemID + self.addedModItemID = self.dstCartItemID + # If drag happened to module which cannot be fit into current slot - consider it as failure + else: + success = False + # And in case of any failures, cancel everything to try to do move instead + if not success: + self.internalHistory.undoAll() + # Just dump module and its charges into cart when copying or moving to cart + if not success: + commands = [] + commands.append(CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=ModuleInfo.fromModule(srcMod, unmutate=True).itemID, amount=1))) + if srcMod.chargeID is not None: + commands.append(CalcAddCartCommand( + fitID=self.fitID, + cartInfo=CartInfo(itemID=srcMod.chargeID, amount=srcMod.numCharges))) + if not self.copy: + commands.append(CalcRemoveLocalModulesCommand( + fitID=self.fitID, + positions=[self.srcModPosition])) + success = self.internalHistory.submitBatch(*commands) + eos.db.flush() + sFit = Fit.getInstance() + sFit.recalc(self.fitID) + self.savedRemovedDummies = sFit.fill(self.fitID) + eos.db.commit() + events = [] + if self.removedModItemID is not None: + events.append(GE.FitChanged(fitIDs=(self.fitID,), action='moddel', typeID=self.removedModItemID)) + if self.addedModItemID is not None: + events.append(GE.FitChanged(fitIDs=(self.fitID,), action='modadd', typeID=self.addedModItemID)) + if not events: + events.append(GE.FitChanged(fitIDs=(self.fitID,))) + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + return success + + def Undo(self): + sFit = Fit.getInstance() + fit = sFit.getFit(self.fitID) + restoreRemovedDummies(fit, self.savedRemovedDummies) + success = self.internalHistory.undoAll() + eos.db.flush() + sFit.recalc(self.fitID) + sFit.fill(self.fitID) + eos.db.commit() + events = [] + if self.addedModItemID is not None: + events.append(GE.FitChanged(fitIDs=(self.fitID,), action='moddel', typeID=self.addedModItemID)) + if self.removedModItemID is not None: + events.append(GE.FitChanged(fitIDs=(self.fitID,), action='modadd', typeID=self.removedModItemID)) + if not events: + events.append(GE.FitChanged(fitIDs=(self.fitID,))) + for event in events: + wx.PostEvent(gui.mainFrame.MainFrame.getInstance(), event) + return success diff --git a/gui/fitCommands/helpers.py b/gui/fitCommands/helpers.py index 58a48b9355..3616c6618e 100644 --- a/gui/fitCommands/helpers.py +++ b/gui/fitCommands/helpers.py @@ -7,6 +7,7 @@ from eos.const import FittingModuleState from eos.saveddata.booster import Booster from eos.saveddata.cargo import Cargo +from eos.saveddata.cart import Cart from eos.saveddata.drone import Drone from eos.saveddata.fighter import Fighter from eos.saveddata.implant import Implant @@ -345,6 +346,30 @@ def toCargo(self): def __repr__(self): return makeReprStr(self, ['itemID', 'amount']) +class CartInfo: + + def __init__(self, itemID, amount): + self.itemID = itemID + self.amount = amount + + @classmethod + def fromCart(cls, cart): + if cart is None: + return None + info = cls( + itemID=cart.itemID, + amount=cart.amount) + return info + + def toCart(self): + item = Market.getInstance().getItem(self.itemID) + cart = Cart(item) + cart.amount = self.amount + return cart + + def __repr__(self): + return makeReprStr(self, ['itemID', 'amount']) + def activeStateLimit(itemIdentity): item = Market.getInstance().getItem(itemIdentity) diff --git a/imgs/gui/cart_small.png b/imgs/gui/cart_small.png new file mode 100644 index 0000000000..8f7b165ae0 Binary files /dev/null and b/imgs/gui/cart_small.png differ diff --git a/pyfa.py b/pyfa.py index 706af752c3..ae8d57ab0a 100755 --- a/pyfa.py +++ b/pyfa.py @@ -67,13 +67,20 @@ def _process_args(self, largs, rargs, values): # Parse command line options usage = "usage: %prog [--root]" parser = PassThroughOptionParser(usage=usage) -parser.add_option("-r", "--root", action="store_true", dest="rootsavedata", help="if you want pyfa to store its data in root folder, use this option", default=False) +parser.add_option("-r", "--root", action="store_true", dest="rootsavedata", help="if you want pyfa to store its data " + "in root folder, use this option", + default=False) parser.add_option("-d", "--debug", action="store_true", dest="debug", help="Set logger to debug level.", default=False) parser.add_option("-t", "--title", action="store", dest="title", help="Set Window Title", default=None) parser.add_option("-s", "--savepath", action="store", dest="savepath", help="Set the folder for savedata", default=None) -parser.add_option("-l", "--logginglevel", action="store", dest="logginglevel", help="Set desired logging level [Critical|Error|Warning|Info|Debug]", default="Error") -parser.add_option("-p", "--profile", action="store", dest="profile_path", help="Set location to save profileing.", default=None) -parser.add_option("-i", "--language", action="store", dest="language", help="Sets the language for pyfa. Overrides user's saved settings. Format: xx_YY (eg: en_US). If translation doesn't exist, defaults to en_US", default=None) +parser.add_option("-l", "--logginglevel", action="store", dest="logginglevel", + help="Set desired logging level [""Critical|Error|Warning|Info|Debug]", default="Error") +parser.add_option("-p", "--profile", action="store", dest="profile_path", help="Set location to save profileing.", + default=None) +parser.add_option("-i", "--language", action="store", dest="language", help="Sets the language for pyfa. Overrides " + "user's saved settings. Format: xx_YY (" + "eg: en_US). If translation doesn't " + "exist, defaults to en_US", default=None) (options, args) = parser.parse_args() @@ -151,7 +158,8 @@ def _process_args(self, largs, rargs, values): ErrorHandler.SetParent(mf) if options.profile_path: - profile_path = os.path.join(options.profile_path, 'pyfa-{}.profile'.format(datetime.datetime.now().strftime('%Y%m%d_%H%M%S'))) + profile_path = os.path.join(options.profile_path, + 'pyfa-{}.profile'.format(datetime.datetime.now().strftime('%Y%m%d_%H%M%S'))) pyfalog.debug("Starting pyfa with a profiler, saving to {}".format(profile_path)) import cProfile diff --git a/requirements.txt b/requirements.txt index 4a95eb125c..d28327b6a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -wxPython == 4.0.6 +wxPython >= 4.0.6 logbook >= 1.0.0 -numpy == 1.19.2 +numpy >= 1.19.2 matplotlib == 3.2.2 python-dateutil requests >= 2.0.0