diff --git a/_development/helpers.py b/_development/helpers.py index ad1447371..3f4635891 100644 --- a/_development/helpers.py +++ b/_development/helpers.py @@ -129,7 +129,8 @@ def Saveddata(): from eos.saveddata.ship import Ship from eos.saveddata.fit import Fit from eos.saveddata.character import Character - from eos.saveddata.module import Module, State + from eos.saveddata.module import Module + from eos.const import FittingModuleState from eos.saveddata.citadel import Citadel from eos.saveddata.booster import Booster @@ -139,7 +140,7 @@ def Saveddata(): 'Fit' : Fit, 'Character': Character, 'Module' : Module, - 'State' : State, + 'State' : FittingModuleState, 'Booster' : Booster, } return helper diff --git a/config.py b/config.py index 7f347eb3c..371adf8e2 100644 --- a/config.py +++ b/config.py @@ -6,7 +6,7 @@ import wx from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \ StreamHandler, TimedRotatingFileHandler, WARNING import hashlib -from eos.const import Slot +from eos.const import FittingSlot from cryptography.fernet import Fernet @@ -50,11 +50,11 @@ LOGLEVEL_MAP = { } slotColourMap = { - Slot.LOW: wx.Colour(250, 235, 204), # yellow = low slots - Slot.MED: wx.Colour(188, 215, 241), # blue = mid slots - Slot.HIGH: wx.Colour(235, 204, 209), # red = high slots - Slot.RIG: '', - Slot.SUBSYSTEM: '' + FittingSlot.LOW: wx.Colour(250, 235, 204), # yellow = low slots + FittingSlot.MED: wx.Colour(188, 215, 241), # blue = mid slots + FittingSlot.HIGH: wx.Colour(235, 204, 209), # red = high slots + FittingSlot.RIG: '', + FittingSlot.SUBSYSTEM: '' } def getClientSecret(): @@ -95,7 +95,7 @@ def defPaths(customSavePath=None): global pyfaPath global savePath global saveDB - global gameDB + global gameDB global saveInRoot global logPath global cipher diff --git a/eos/config.py b/eos/config.py index 2f7807a36..641a00ba9 100644 --- a/eos/config.py +++ b/eos/config.py @@ -21,13 +21,14 @@ if istravis is True or hasattr(sys, '_called_from_test'): # Running in Travis. Run saveddata database in memory. saveddata_connectionstring = 'sqlite:///:memory:' else: - saveddata_connectionstring = 'sqlite:///' + realpath(join(dirname(abspath(__file__)), "..", "saveddata", "saveddata-py3-db.db")) + saveddata_connectionstring = 'sqlite:///' + realpath(join(dirname(abspath(__file__)), "..", "saveddata", "saveddata.db")) pyfalog.debug("Saveddata connection string: {0}", saveddata_connectionstring) settings = { "useStaticAdaptiveArmorHardener": False, "strictSkillLevels": True, + "globalDefaultSpoolupPercentage": 1.0 } # Autodetect path, only change if the autodetection bugs out. diff --git a/eos/const.py b/eos/const.py index 95dd8a888..4c71d76c5 100644 --- a/eos/const.py +++ b/eos/const.py @@ -1,8 +1,30 @@ -from eos.enum import Enum +# ============================================================================= +# Copyright (C) 2019 Ryan Holmes +# +# This file is part of pyfa. +# +# pyfa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pyfa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyfa. If not, see . +# ============================================================================= + +from enum import IntEnum,unique - -class Slot(Enum): +@unique +class FittingSlot(IntEnum): + """ + Contains slots for ship fittings + """ # These are self-explanatory LOW = 1 MED = 2 @@ -24,3 +46,50 @@ class Slot(Enum): FS_LIGHT = 13 FS_SUPPORT = 14 FS_HEAVY = 15 + + +@unique +class ImplantLocation(IntEnum): + """ + Contains location of the implant + """ + FIT = 0 + CHARACTER = 1 + + +@unique +class CalcType(IntEnum): + """ + Contains location of the calculation + """ + LOCAL = 0 + PROJECTED = 1 + COMMAND = 2 + + +@unique +class FittingModuleState(IntEnum): + """ + Contains the state of a fitting module + """ + OFFLINE = -1 + ONLINE = 0 + ACTIVE = 1 + OVERHEATED = 2 + + +@unique +class FittingHardpoint(IntEnum): + """ + Contains the types of a fitting hardpoint + """ + NONE = 0 + MISSILE = 1 + TURRET = 2 + + +@unique +class SpoolType(IntEnum): + SCALE = 0 # [0..1] + TIME = 1 # Expressed via time in seconds since spool up started + CYCLES = 2 # Expressed in amount of cycles since spool up started diff --git a/eos/db/gamedata/item.py b/eos/db/gamedata/item.py index fd7be477d..2683a41e9 100644 --- a/eos/db/gamedata/item.py +++ b/eos/db/gamedata/item.py @@ -41,8 +41,7 @@ items_table = Table("invtypes", gamedata_meta, Column("iconID", Integer), Column("graphicID", Integer), Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True), - Column("replaceSame", String), - Column("replaceBetter", String)) + Column("replacements", String)) from .metaGroup import metatypes_table # noqa from .traits import traits_table # noqa diff --git a/eos/db/saveddata/price.py b/eos/db/saveddata/price.py index 8abd07132..e0e0f530a 100644 --- a/eos/db/saveddata/price.py +++ b/eos/db/saveddata/price.py @@ -32,6 +32,4 @@ prices_table = Table("prices", saveddata_meta, Column("status", Integer, nullable=False)) -mapper(Price, prices_table, properties={ - "_Price__price": prices_table.c.price, -}) +mapper(Price, prices_table) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 448584420..ae66e9285 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -21,6 +21,7 @@ import sys from sqlalchemy.sql import and_ from sqlalchemy import desc, select +from sqlalchemy import func from eos.db import saveddata_session, sd_lock from eos.db.saveddata.fit import projectedFits_table @@ -283,6 +284,12 @@ def countAllFits(): return count +def countFitGroupedByShip(): + with sd_lock: + count = eos.db.saveddata_session.query(Fit.shipID, func.count(Fit.shipID)).group_by(Fit.shipID).all() + return count + + def countFitsWithShip(lookfor, ownerID=None, where=None, eager=None): """ Get all the fits using a certain ship. diff --git a/eos/effects/effect3380.py b/eos/effects/effect3380.py index f8ce29992..5a5878fd4 100644 --- a/eos/effects/effect3380.py +++ b/eos/effects/effect3380.py @@ -7,7 +7,7 @@ # # Used by: # Modules from group: Warp Disrupt Field Generator (7 of 7) -from eos.saveddata.module import State +from eos.const import FittingModuleState type = "projected", "active" runTime = "early" @@ -19,10 +19,10 @@ def handler(fit, module, context): fit.ship.increaseItemAttr("warpScrambleStatus", module.getModifiedItemAttr("warpScrambleStrength")) if module.charge is not None and module.charge.ID == 45010: for mod in fit.modules: - if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > State.ONLINE: - mod.state = State.ONLINE - if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE: - mod.state = State.ONLINE + if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > FittingModuleState.ONLINE: + mod.state = FittingModuleState.ONLINE + if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > FittingModuleState.ONLINE: + mod.state = FittingModuleState.ONLINE else: if module.charge is None: fit.ship.boostItemAttr("mass", module.getModifiedItemAttr("massBonusPercentage")) diff --git a/eos/effects/effect5934.py b/eos/effects/effect5934.py index 0985d4673..4b3ee34cb 100644 --- a/eos/effects/effect5934.py +++ b/eos/effects/effect5934.py @@ -2,7 +2,7 @@ # # Used by: # Modules named like: Warp Scrambler (27 of 27) -from eos.saveddata.module import State +from eos.const import FittingModuleState runTime = "early" type = "projected", "active" @@ -16,10 +16,10 @@ def handler(fit, module, context): # this is such a dirty hack for mod in fit.modules: - if not mod.isEmpty and mod.state > State.ONLINE and ( + if not mod.isEmpty and mod.state > FittingModuleState.ONLINE and ( mod.item.requiresSkill("Micro Jump Drive Operation") or mod.item.requiresSkill("High Speed Maneuvering") ): - mod.state = State.ONLINE - if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE: - mod.state = State.ONLINE + mod.state = FittingModuleState.ONLINE + if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > FittingModuleState.ONLINE: + mod.state = FittingModuleState.ONLINE diff --git a/eos/effects/effect6187.py b/eos/effects/effect6187.py index 67e55e7ad..2ee279d44 100644 --- a/eos/effects/effect6187.py +++ b/eos/effects/effect6187.py @@ -2,15 +2,15 @@ # # Used by: # Modules from group: Energy Neutralizer (54 of 54) -from eos.saveddata.module import State +from eos.const import FittingModuleState from eos.modifiedAttributeDict import ModifiedAttributeDict type = "active", "projected" def handler(fit, src, context, **kwargs): - if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or - hasattr(src, "amountActive")): + if "projected" in context and ((hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or + hasattr(src, "amountActive")): amount = src.getModifiedItemAttr("energyNeutralizerAmount") if 'effect' in kwargs: diff --git a/eos/effects/effect6216.py b/eos/effects/effect6216.py index c9cdd769e..284ed616e 100644 --- a/eos/effects/effect6216.py +++ b/eos/effects/effect6216.py @@ -2,7 +2,7 @@ # # Used by: # Structure Modules from group: Structure Energy Neutralizer (5 of 5) -from eos.saveddata.module import State +from eos.const import FittingModuleState from eos.modifiedAttributeDict import ModifiedAttributeDict type = "active", "projected" @@ -11,7 +11,7 @@ type = "active", "projected" def handler(fit, src, context, **kwargs): amount = 0 if "projected" in context: - if (hasattr(src, "state") and src.state >= State.ACTIVE) or hasattr(src, "amountActive"): + if (hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or hasattr(src, "amountActive"): amount = src.getModifiedItemAttr("energyNeutralizerAmount") if 'effect' in kwargs: diff --git a/eos/effects/effect6222.py b/eos/effects/effect6222.py index e6ab9536e..4b342e0ec 100644 --- a/eos/effects/effect6222.py +++ b/eos/effects/effect6222.py @@ -2,7 +2,7 @@ # # Used by: # Structure Modules from group: Structure Warp Scrambler (2 of 2) -from eos.saveddata.module import State +from eos.const import FittingModuleState # Not used by any item runTime = "early" @@ -14,7 +14,7 @@ def handler(fit, module, context): fit.ship.increaseItemAttr("warpScrambleStatus", module.getModifiedItemAttr("warpScrambleStrength")) if module.charge is not None and module.charge.ID == 47336: for mod in fit.modules: - if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > State.ONLINE: - mod.state = State.ONLINE - if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE: - mod.state = State.ONLINE + if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > FittingModuleState.ONLINE: + mod.state = FittingModuleState.ONLINE + if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > FittingModuleState.ONLINE: + mod.state = FittingModuleState.ONLINE diff --git a/eos/effects/effect6477.py b/eos/effects/effect6477.py index 1be85f6bd..6f9cd2f5e 100644 --- a/eos/effects/effect6477.py +++ b/eos/effects/effect6477.py @@ -3,15 +3,15 @@ # Used by: # Module: Energy Neutralization Burst Projector # Structure Module: Standup Energy Neutralization Burst Projector -from eos.saveddata.module import State +from eos.const import FittingModuleState from eos.modifiedAttributeDict import ModifiedAttributeDict type = "active", "projected" def handler(fit, src, context, **kwargs): - if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or - hasattr(src, "amountActive")): + if "projected" in context and ((hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or + hasattr(src, "amountActive")): amount = src.getModifiedItemAttr("energyNeutralizerAmount") if 'effect' in kwargs: diff --git a/eos/effects/effect6691.py b/eos/effects/effect6691.py index 751cd540c..ade55d190 100644 --- a/eos/effects/effect6691.py +++ b/eos/effects/effect6691.py @@ -2,15 +2,15 @@ # # Used by: # Drones from group: Energy Neutralizer Drone (3 of 3) -from eos.saveddata.module import State +from eos.const import FittingModuleState from eos.modifiedAttributeDict import ModifiedAttributeDict type = "active", "projected" def handler(fit, src, context, **kwargs): - if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or - hasattr(src, "amountActive")): + if "projected" in context and ((hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or + hasattr(src, "amountActive")): amount = src.getModifiedItemAttr("energyNeutralizerAmount") time = src.getModifiedItemAttr("energyNeutralizerDuration") diff --git a/eos/effects/effect7166.py b/eos/effects/effect7166.py index ec98c1e23..94a53be2b 100644 --- a/eos/effects/effect7166.py +++ b/eos/effects/effect7166.py @@ -4,6 +4,7 @@ # Modules from group: Mutadaptive Remote Armor Repairer (5 of 5) +import eos.config from eos.utils.spoolSupport import SpoolType, SpoolOptions, calculateSpoolup, resolveSpoolOptions @@ -17,8 +18,7 @@ def handler(fit, container, context, **kwargs): cycleTime = container.getModifiedItemAttr("duration") / 1000.0 repSpoolMax = container.getModifiedItemAttr("repairMultiplierBonusMax") repSpoolPerCycle = container.getModifiedItemAttr("repairMultiplierBonusPerCycle") - # TODO: fetch spoolup option - defaultSpoolValue = 1 + defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] spoolType, spoolAmount = resolveSpoolOptions(SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False), container) rps = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, spoolType, spoolAmount)[0]) / cycleTime rpsPreSpool = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, SpoolType.SCALE, 0)[0]) / cycleTime diff --git a/eos/effects/structurecombatrigsecuritymodification.py b/eos/effects/structurecombatrigsecuritymodification.py new file mode 100644 index 000000000..dcbc93d39 --- /dev/null +++ b/eos/effects/structurecombatrigsecuritymodification.py @@ -0,0 +1,17 @@ +runTime = "early" +type = "passive" + + +def handler(fit, module, context): + secModifier = module.getModifiedItemAttr("securityModifier") + module.multiplyItemAttr("structureRigDoomsdayDamageLossTargetBonus", secModifier) + module.multiplyItemAttr("structureRigScanResBonus", secModifier) + module.multiplyItemAttr("structureRigPDRangeBonus", secModifier) + module.multiplyItemAttr("structureRigPDCapUseBonus", secModifier) + module.multiplyItemAttr("structureRigMissileExploVeloBonus", secModifier) + module.multiplyItemAttr("structureRigMissileVelocityBonus", secModifier) + module.multiplyItemAttr("structureRigEwarOptimalBonus", secModifier) + module.multiplyItemAttr("structureRigEwarFalloffBonus", secModifier) + module.multiplyItemAttr("structureRigEwarCapUseBonus", secModifier) + module.multiplyItemAttr("structureRigMissileExplosionRadiusBonus", secModifier) + module.multiplyItemAttr("structureRigMaxTargetRangeBonus", secModifier) diff --git a/eos/enum.py b/eos/enum.py deleted file mode 100644 index 12e8e09e1..000000000 --- a/eos/enum.py +++ /dev/null @@ -1,23 +0,0 @@ -class Enum(object): - def __init__(self): - pass - - @classmethod - def getTypes(cls): - for stuff in cls.__dict__: - if stuff.upper() == stuff: - yield stuff - - @classmethod - def getName(cls, v): - map = getattr(cls, "_map", None) - if map is None: - map = cls._map = {} - for type in cls.getTypes(): - map[cls.getValue(type)] = type - - return map.get(v) - - @classmethod - def getValue(cls, type): - return cls.__dict__[type] diff --git a/eos/graph/fitDps.py b/eos/graph/fitDps.py index a8d6c4ec3..56f0089e1 100644 --- a/eos/graph/fitDps.py +++ b/eos/graph/fitDps.py @@ -20,7 +20,7 @@ from math import log, sin, radians, exp from eos.graph import Graph -from eos.saveddata.module import State, Hardpoint +from eos.const import FittingModuleState, FittingHardpoint from logbook import Logger pyfalog = Logger(__name__) @@ -46,7 +46,7 @@ class FitDpsGraph(Graph): abssort = lambda _val: -abs(_val - 1) for mod in fit.modules: - if not mod.isEmpty and mod.state >= State.ACTIVE: + if not mod.isEmpty and mod.state >= FittingModuleState.ACTIVE: if "remoteTargetPaintFalloff" in mod.item.effects or "structureModuleEffectTargetPainter" in mod.item.effects: ew['signatureRadius'].append( 1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) * self.calculateModuleMultiplier( @@ -76,12 +76,12 @@ class FitDpsGraph(Graph): for mod in fit.modules: dps = mod.getDps(targetResists=fit.targetResists).total - if mod.hardpoint == Hardpoint.TURRET: - if mod.state >= State.ACTIVE: + if mod.hardpoint == FittingHardpoint.TURRET: + if mod.state >= FittingModuleState.ACTIVE: total += dps * self.calculateTurretMultiplier(mod, data) - elif mod.hardpoint == Hardpoint.MISSILE: - if mod.state >= State.ACTIVE and mod.maxRange is not None and mod.maxRange >= distance: + elif mod.hardpoint == FittingHardpoint.MISSILE: + if mod.state >= FittingModuleState.ACTIVE and mod.maxRange is not None and mod.maxRange >= distance: total += dps * self.calculateMissileMultiplier(mod, data) if distance <= fit.extraAttributes["droneControlRange"]: diff --git a/eos/saveddata/booster.py b/eos/saveddata/booster.py index 4c6bf23db..3258fcfe3 100644 --- a/eos/saveddata/booster.py +++ b/eos/saveddata/booster.py @@ -30,6 +30,7 @@ pyfalog = Logger(__name__) class Booster(HandledItem, ItemAttrShortcut): + def __init__(self, item): self.__item = item @@ -147,3 +148,17 @@ class Booster(HandledItem, ItemAttrShortcut): copyEffect.active = sideEffect.active return copy + + def rebase(self, item): + active = self.active + sideEffectStates = {se.effectID: se.active for se in self.sideEffects} + Booster.__init__(self, item) + self.active = active + for sideEffect in self.sideEffects: + if sideEffect.effectID in sideEffectStates: + sideEffect.active = sideEffectStates[sideEffect.effectID] + + def __repr__(self): + return "Booster(ID={}, name={}) at {}".format( + self.item.ID, self.item.name, hex(id(self)) + ) diff --git a/eos/saveddata/cargo.py b/eos/saveddata/cargo.py index 11c017370..3fbfe29f0 100644 --- a/eos/saveddata/cargo.py +++ b/eos/saveddata/cargo.py @@ -89,3 +89,13 @@ class Cargo(HandledItem, ItemAttrShortcut): copy = Cargo(self.item) copy.amount = self.amount return copy + + def rebase(self, item): + amount = self.amount + Cargo.__init__(self, item) + self.amount = amount + + def __repr__(self): + return "Cargo(ID={}, name={}) at {}".format( + self.item.ID, self.item.name, hex(id(self)) + ) diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index 2fec27fc9..65ef5d327 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -296,6 +296,13 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): copy.amountActive = self.amountActive return copy + def rebase(self, item): + amount = self.amount + amountActive = self.amountActive + Drone.__init__(self, item) + self.amount = amount + self.amountActive = amountActive + def fits(self, fit): fitDroneGroupLimits = set() for i in range(1, 3): diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index 4b929d594..8024b0681 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -25,8 +25,8 @@ import eos.db from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.saveddata.fighterAbility import FighterAbility -from eos.saveddata.module import Slot from eos.utils.stats import DmgTypes +from eos.const import FittingSlot pyfalog = Logger(__name__) @@ -116,12 +116,12 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def __calculateSlot(self, item): types = { - "Light" : Slot.F_LIGHT, - "Support": Slot.F_SUPPORT, - "Heavy" : Slot.F_HEAVY, - "StandupLight": Slot.FS_LIGHT, - "StandupSupport": Slot.FS_SUPPORT, - "StandupHeavy": Slot.FS_HEAVY + "Light" : FittingSlot.F_LIGHT, + "Support": FittingSlot.F_SUPPORT, + "Heavy" : FittingSlot.F_HEAVY, + "StandupLight": FittingSlot.FS_LIGHT, + "StandupSupport": FittingSlot.FS_SUPPORT, + "StandupHeavy": FittingSlot.FS_HEAVY } for t, slot in types.items(): @@ -355,6 +355,17 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): copyAbility.active = ability.active return copy + def rebase(self, item): + amount = self.amount + active = self.active + abilityEffectStates = {a.effectID: a.active for a in self.abilities} + Fighter.__init__(self, item) + self.amount = amount + self.active = active + for ability in self.abilities: + if ability.effectID in abilityEffectStates: + ability.active = abilityEffectStates[ability.effectID] + def fits(self, fit): # If ships doesn't support this type of fighter, don't add it if fit.getNumSlots(self.slot) == 0: diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index f33a78651..c31c17696 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -28,28 +28,19 @@ from sqlalchemy.orm import validates, reconstructor import eos.db from eos import capSim from eos.effectHandlerHelpers import HandledModuleList, HandledDroneCargoList, HandledImplantBoosterList, HandledProjectedDroneList, HandledProjectedModList -from eos.enum import Enum +from eos.const import ImplantLocation, CalcType, FittingSlot from eos.saveddata.ship import Ship from eos.saveddata.drone import Drone from eos.saveddata.character import Character from eos.saveddata.citadel import Citadel -from eos.saveddata.module import Module, State, Slot, Hardpoint +from eos.const import FittingModuleState, FittingHardpoint +from eos.saveddata.module import Module from eos.utils.stats import DmgTypes from logbook import Logger + pyfalog = Logger(__name__) -class ImplantLocation(Enum): - FIT = 0 - CHARACTER = 1 - - -class CalcType(Enum): - LOCAL = 0 - PROJECTED = 1 - COMMAND = 2 - - class Fit(object): """Represents a fitting, with modules, ship, implants, etc.""" @@ -393,6 +384,32 @@ class Fit(object): else: return val + def canFit(self, item): + # Whereas Module.fits() deals with current state of the fit in order to determine if somethign fits (for example maxGroupFitted which can be modified by effects), + # this function should be used against Items to see if the item is even allowed on the fit with rules that don't change + + fitsOnType = set() + fitsOnGroup = set() + + shipType = item.attributes.get("fitsToShipType", None) + if shipType is not None: + fitsOnType.add(shipType.value) + + fitsOnType.update([item.attributes[attr].value for attr in item.attributes if attr.startswith("canFitShipType")]) + fitsOnGroup.update([item.attributes[attr].value for attr in item.attributes if attr.startswith("canFitShipGroup")]) + + if (len(fitsOnGroup) > 0 or len(fitsOnType) > 0) \ + and self.ship.item.group.ID not in fitsOnGroup \ + and self.ship.item.ID not in fitsOnType: + return False + + # Citadel modules are now under a new category, so we can check this to ensure only structure modules can fit on a citadel + if isinstance(self.ship, Citadel) and item.category.name != "Structure Module" or \ + not isinstance(self.ship, Citadel) and item.category.name == "Structure Module": + return False + + return True + def clear(self, projected=False, command=False): self.__effectiveTank = None self.__weaponDpsMap = {} @@ -741,7 +758,7 @@ class Fit(object): The type of calculation our current iteration is in. This helps us determine the interactions between fits that rely on others for proper calculations """ - pyfalog.info("Starting fit calculation on: {0}, calc: {1}", repr(self), CalcType.getName(type)) + pyfalog.info("Starting fit calculation on: {0}, calc: {1}", repr(self), CalcType(type).name) # If we are projecting this fit onto another one, collect the projection info for later use @@ -901,7 +918,7 @@ class Fit(object): if self.ship is None: return - for slotType in (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE): + for slotType in (FittingSlot.LOW.value, FittingSlot.MED.value, FittingSlot.HIGH.value, FittingSlot.RIG.value, FittingSlot.SUBSYSTEM.value, FittingSlot.SERVICE.value): amount = self.getSlotsFree(slotType, True) if amount > 0: for _ in range(int(amount)): @@ -948,7 +965,7 @@ class Fit(object): def getItemAttrOnlineSum(dict, attr): amount = 0 for mod in dict: - add = mod.getModifiedItemAttr(attr) if mod.state >= State.ONLINE else None + add = mod.getModifiedItemAttr(attr) if mod.state >= FittingModuleState.ONLINE else None if add is not None: amount += add @@ -967,29 +984,29 @@ class Fit(object): for mod in chain(self.modules, self.fighters): if mod.slot is type and (not getattr(mod, "isEmpty", False) or countDummies): - if type in (Slot.F_HEAVY, Slot.F_SUPPORT, Slot.F_LIGHT, Slot.FS_HEAVY, Slot.FS_LIGHT, Slot.FS_SUPPORT) and not mod.active: + if type in (FittingSlot.F_HEAVY, FittingSlot.F_SUPPORT, FittingSlot.F_LIGHT, FittingSlot.FS_HEAVY, FittingSlot.FS_LIGHT, FittingSlot.FS_SUPPORT) and not mod.active: continue amount += 1 return amount slots = { - Slot.LOW : "lowSlots", - Slot.MED : "medSlots", - Slot.HIGH : "hiSlots", - Slot.RIG : "rigSlots", - Slot.SUBSYSTEM: "maxSubSystems", - Slot.SERVICE : "serviceSlots", - Slot.F_LIGHT : "fighterLightSlots", - Slot.F_SUPPORT: "fighterSupportSlots", - Slot.F_HEAVY : "fighterHeavySlots", - Slot.FS_LIGHT: "fighterStandupLightSlots", - Slot.FS_SUPPORT: "fighterStandupSupportSlots", - Slot.FS_HEAVY: "fighterStandupHeavySlots", + FittingSlot.LOW : "lowSlots", + FittingSlot.MED : "medSlots", + FittingSlot.HIGH : "hiSlots", + FittingSlot.RIG : "rigSlots", + FittingSlot.SUBSYSTEM: "maxSubSystems", + FittingSlot.SERVICE : "serviceSlots", + FittingSlot.F_LIGHT : "fighterLightSlots", + FittingSlot.F_SUPPORT: "fighterSupportSlots", + FittingSlot.F_HEAVY : "fighterHeavySlots", + FittingSlot.FS_LIGHT: "fighterStandupLightSlots", + FittingSlot.FS_SUPPORT: "fighterStandupSupportSlots", + FittingSlot.FS_HEAVY: "fighterStandupHeavySlots", } def getSlotsFree(self, type, countDummies=False): - if type in (Slot.MODE, Slot.SYSTEM): + if type in (FittingSlot.MODE, FittingSlot.SYSTEM): # These slots don't really exist, return default 0 return 0 @@ -1001,12 +1018,12 @@ class Fit(object): return self.ship.getModifiedItemAttr(self.slots[type]) or 0 def getHardpointsFree(self, type): - if type == Hardpoint.NONE: + if type == FittingHardpoint.NONE: return 1 - elif type == Hardpoint.TURRET: - return self.ship.getModifiedItemAttr('turretSlotsLeft') - self.getHardpointsUsed(Hardpoint.TURRET) - elif type == Hardpoint.MISSILE: - return self.ship.getModifiedItemAttr('launcherSlotsLeft') - self.getHardpointsUsed(Hardpoint.MISSILE) + elif type == FittingHardpoint.TURRET: + return self.ship.getModifiedItemAttr('turretSlotsLeft') - self.getHardpointsUsed(FittingHardpoint.TURRET) + elif type == FittingHardpoint.MISSILE: + return self.ship.getModifiedItemAttr('launcherSlotsLeft') - self.getHardpointsUsed(FittingHardpoint.MISSILE) else: raise ValueError("%d is not a valid value for Hardpoint Enum", type) @@ -1168,7 +1185,7 @@ class Fit(object): capUsed = 0 capAdded = 0 for mod in self.modules: - if mod.state >= State.ACTIVE: + if mod.state >= FittingModuleState.ACTIVE: if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0: cycleTime = mod.rawCycleTime or 0 reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0 @@ -1182,7 +1199,7 @@ class Fit(object): capAdded -= capNeed # If this is a turret, don't stagger activations - disableStagger = mod.hardpoint == Hardpoint.TURRET + disableStagger = mod.hardpoint == FittingHardpoint.TURRET drains.append((int(fullCycleTime), mod.getModifiedItemAttr("capacitorNeed") or 0, mod.numShots or 0, disableStagger, reloadTime)) diff --git a/eos/saveddata/implant.py b/eos/saveddata/implant.py index d61b9c43b..5da13d28c 100644 --- a/eos/saveddata/implant.py +++ b/eos/saveddata/implant.py @@ -115,6 +115,11 @@ class Implant(HandledItem, ItemAttrShortcut): copy.active = self.active return copy + def rebase(self, item): + active = self.active + Implant.__init__(self, item) + self.active = active + def __repr__(self): return "Implant(ID={}, name={}) at {}".format( self.item.ID, self.item.name, hex(id(self)) diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 25b15e147..28ee586ec 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -23,9 +23,8 @@ from logbook import Logger from sqlalchemy.orm import reconstructor, validates import eos.db -from eos.const import Slot +from eos.const import FittingModuleState, FittingHardpoint, FittingSlot from eos.effectHandlerHelpers import HandledCharge, HandledItem -from eos.enum import Enum from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict from eos.saveddata.citadel import Citadel from eos.saveddata.mutator import Mutator @@ -35,44 +34,30 @@ from eos.utils.stats import DmgTypes pyfalog = Logger(__name__) - -class State(Enum): - OFFLINE = -1 - ONLINE = 0 - ACTIVE = 1 - OVERHEATED = 2 - - ProjectedMap = { - State.OVERHEATED: State.ACTIVE, - State.ACTIVE: State.OFFLINE, - State.OFFLINE: State.ACTIVE, - State.ONLINE: State.ACTIVE # Just in case + FittingModuleState.OVERHEATED: FittingModuleState.ACTIVE, + FittingModuleState.ACTIVE: FittingModuleState.OFFLINE, + FittingModuleState.OFFLINE: FittingModuleState.ACTIVE, + FittingModuleState.ONLINE: FittingModuleState.ACTIVE # Just in case } # Old state : New State LocalMap = { - State.OVERHEATED: State.ACTIVE, - State.ACTIVE: State.ONLINE, - State.OFFLINE: State.ONLINE, - State.ONLINE: State.ACTIVE + FittingModuleState.OVERHEATED: FittingModuleState.ACTIVE, + FittingModuleState.ACTIVE: FittingModuleState.ONLINE, + FittingModuleState.OFFLINE: FittingModuleState.ONLINE, + FittingModuleState.ONLINE: FittingModuleState.ACTIVE } # For system effects. They should only ever be online or offline ProjectedSystem = { - State.OFFLINE: State.ONLINE, - State.ONLINE: State.OFFLINE + FittingModuleState.OFFLINE: FittingModuleState.ONLINE, + FittingModuleState.ONLINE: FittingModuleState.OFFLINE } -class Hardpoint(Enum): - NONE = 0 - MISSILE = 1 - TURRET = 2 - - class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): """An instance of this class represents a module together with its charge and modified attributes""" MINING_ATTRIBUTES = ("miningAmount",) @@ -104,7 +89,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): self.__charge = None self.projected = False - self.state = State.ONLINE + self.state = FittingModuleState.ONLINE self.build() @reconstructor @@ -153,7 +138,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): self.__reloadTime = None self.__reloadForce = None self.__chargeCycles = None - self.__hardpoint = Hardpoint.NONE + self.__hardpoint = FittingHardpoint.NONE self.__itemModifiedAttributes = ModifiedAttributeDict(parent=self) self.__chargeModifiedAttributes = ModifiedAttributeDict(parent=self) self.__slot = self.dummySlot # defaults to None @@ -396,7 +381,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if self.isEmpty: self.__miningyield = 0 else: - if self.state >= State.ACTIVE: + if self.state >= FittingModuleState.ACTIVE: volley = self.getModifiedItemAttr("specialtyMiningAmount") or self.getModifiedItemAttr( "miningAmount") or 0 if volley: @@ -410,7 +395,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return self.__miningyield def getVolley(self, spoolOptions=None, targetResists=None, ignoreState=False): - if self.isEmpty or (self.state < State.ACTIVE and not ignoreState): + if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState): return DmgTypes(0, 0, 0, 0) if self.__baseVolley is None: dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr @@ -448,7 +433,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return dps def getRemoteReps(self, spoolOptions=None, ignoreState=False): - if self.isEmpty or (self.state < State.ACTIVE and not ignoreState): + if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState): return None, 0 def getBaseRemoteReps(module): @@ -554,34 +539,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return fits def __fitRestrictions(self, fit, hardpointLimit=True): - # Check ship type restrictions - fitsOnType = set() - fitsOnGroup = set() - shipType = self.getModifiedItemAttr("fitsToShipType", None) - if shipType is not None: - fitsOnType.add(shipType) - - for attr in list(self.itemModifiedAttributes.keys()): - if attr.startswith("canFitShipType"): - shipType = self.getModifiedItemAttr(attr, None) - if shipType is not None: - fitsOnType.add(shipType) - - for attr in list(self.itemModifiedAttributes.keys()): - if attr.startswith("canFitShipGroup"): - shipGroup = self.getModifiedItemAttr(attr, None) - if shipGroup is not None: - fitsOnGroup.add(shipGroup) - - if (len(fitsOnGroup) > 0 or len(fitsOnType) > 0) \ - and fit.ship.item.group.ID not in fitsOnGroup \ - and fit.ship.item.ID not in fitsOnType: - return False - - # Citadel modules are now under a new category, so we can check this to ensure only structure modules can fit on a citadel - if isinstance(fit.ship, Citadel) and self.item.category.name != "Structure Module" or \ - not isinstance(fit.ship, Citadel) and self.item.category.name == "Structure Module": + if not fit.canFit(self.item): return False # EVE doesn't let capital modules be fit onto subcapital hulls. Confirmed by CCP Larrikin that this is dictated @@ -590,14 +549,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return False # If the mod is a subsystem, don't let two subs in the same slot fit - if self.slot == Slot.SUBSYSTEM: + if self.slot == FittingSlot.SUBSYSTEM: subSlot = self.getModifiedItemAttr("subSystemSlot") for mod in fit.modules: if mod.getModifiedItemAttr("subSystemSlot") == subSlot: return False # Check rig sizes - if self.slot == Slot.RIG: + if self.slot == FittingSlot.RIG: if self.getModifiedItemAttr("rigSize") != fit.ship.getModifiedItemAttr("rigSize"): return False @@ -627,9 +586,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): # Check if we're within bounds if state < -1 or state > 2: return False - elif state >= State.ACTIVE and not self.item.isType("active"): + elif state >= FittingModuleState.ACTIVE and not self.item.isType("active"): return False - elif state == State.OVERHEATED and not self.item.isType("overheat"): + elif state == FittingModuleState.OVERHEATED and not self.item.isType("overheat"): return False else: return True @@ -641,7 +600,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): # If we're going to set module to offline or online for local modules or offline for projected, # it should be fine for all cases item = self.item - if (state <= State.ONLINE and projectedOnto is None) or (state <= State.OFFLINE): + if (state <= FittingModuleState.ONLINE and projectedOnto is None) or (state <= FittingModuleState.OFFLINE): return True # Check if the local module is over it's max limit; if it's not, we're fine @@ -655,7 +614,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): group = item.group.name for mod in self.owner.modules: currItem = getattr(mod, "item", None) - if mod.state >= State.ACTIVE and currItem is not None and currItem.group.name == group: + if mod.state >= FittingModuleState.ACTIVE and currItem is not None and currItem.group.name == group: currActive += 1 if currActive > maxGroupActive: break @@ -718,28 +677,28 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @staticmethod def __calculateHardpoint(item): effectHardpointMap = { - "turretFitted" : Hardpoint.TURRET, - "launcherFitted": Hardpoint.MISSILE + "turretFitted" : FittingHardpoint.TURRET, + "launcherFitted": FittingHardpoint.MISSILE } if item is None: - return Hardpoint.NONE + return FittingHardpoint.NONE for effectName, slot in effectHardpointMap.items(): if effectName in item.effects: return slot - return Hardpoint.NONE + return FittingHardpoint.NONE @staticmethod def calculateSlot(item): effectSlotMap = { - "rigSlot" : Slot.RIG, - "loPower" : Slot.LOW, - "medPower" : Slot.MED, - "hiPower" : Slot.HIGH, - "subSystem" : Slot.SUBSYSTEM, - "serviceSlot": Slot.SERVICE + "rigSlot" : FittingSlot.RIG.value, + "loPower" : FittingSlot.LOW.value, + "medPower" : FittingSlot.MED.value, + "hiPower" : FittingSlot.HIGH.value, + "subSystem" : FittingSlot.SUBSYSTEM.value, + "serviceSlot": FittingSlot.SERVICE.value } if item is None: return None @@ -747,7 +706,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if effectName in item.effects: return slot if item.group.name in Module.SYSTEM_GROUPS: - return Slot.SYSTEM + return FittingSlot.SYSTEM return None @@ -801,8 +760,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if effect.runTime == runTime and \ effect.activeByDefault and \ (effect.isType("offline") or - (effect.isType("passive") and self.state >= State.ONLINE) or - (effect.isType("active") and self.state >= State.ACTIVE)) and \ + (effect.isType("passive") and self.state >= FittingModuleState.ONLINE) or + (effect.isType("active") and self.state >= FittingModuleState.ACTIVE)) and \ (not gang or (gang and effect.isType("gang"))): chargeContext = ("moduleCharge",) @@ -815,7 +774,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): effect.handler(fit, self, chargeContext) if self.item: - if self.state >= State.OVERHEATED: + if self.state >= FittingModuleState.OVERHEATED: for effect in self.item.effects.values(): if effect.runTime == runTime and \ effect.isType("overheat") \ @@ -828,8 +787,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if effect.runTime == runTime and \ effect.activeByDefault and \ (effect.isType("offline") or - (effect.isType("passive") and self.state >= State.ONLINE) or - (effect.isType("active") and self.state >= State.ACTIVE)) \ + (effect.isType("passive") and self.state >= FittingModuleState.ONLINE) or + (effect.isType("active") and self.state >= FittingModuleState.ACTIVE)) \ and ((projected and effect.isType("projected")) or not projected) \ and ((gang and effect.isType("gang")) or not gang): try: @@ -904,7 +863,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def capUse(self): capNeed = self.getModifiedItemAttr("capacitorNeed") - if capNeed and self.state >= State.ACTIVE: + if capNeed and self.state >= FittingModuleState.ACTIVE: cycleTime = self.cycleTime if cycleTime > 0: capUsed = capNeed / (cycleTime / 1000.0) @@ -916,10 +875,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def getProposedState(mod, click, proposedState=None): # todo: instead of passing in module, make this a instanced function. pyfalog.debug("Get proposed state for module.") - if mod.slot == Slot.SUBSYSTEM or mod.isEmpty: - return State.ONLINE + if mod.slot == FittingSlot.SUBSYSTEM or mod.isEmpty: + return FittingModuleState.ONLINE - if mod.slot == Slot.SYSTEM: + if mod.slot == FittingSlot.SYSTEM: transitionMap = ProjectedSystem else: transitionMap = ProjectedMap if mod.projected else LocalMap @@ -929,9 +888,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if proposedState is not None: state = proposedState elif click == "right": - state = State.OVERHEATED + state = FittingModuleState.OVERHEATED elif click == "ctrl": - state = State.OFFLINE + state = FittingModuleState.OFFLINE else: state = transitionMap[currState] if not mod.isValidState(state): @@ -956,6 +915,16 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return copy + def rebase(self, item): + state = self.state + charge = self.charge + Module.__init__(self, item, self.baseItem, self.mutaplasmid) + self.state = state + if self.isValidCharge(charge): + self.charge = charge + for x in self.mutators.values(): + Mutator(self, x.attribute, x.value) + def __repr__(self): if self.item: return "Module(ID={}, name={}) at {}".format( diff --git a/eos/saveddata/price.py b/eos/saveddata/price.py index a2a630c30..0a2a7ce9d 100644 --- a/eos/saveddata/price.py +++ b/eos/saveddata/price.py @@ -19,41 +19,56 @@ # =============================================================================== -import time from enum import IntEnum, unique +from time import time from logbook import Logger +VALIDITY = 24 * 60 * 60 # Price validity period, 24 hours +REREQUEST = 4 * 60 * 60 # Re-request delay for failed fetches, 4 hours +TIMEOUT = 15 * 60 # Network timeout delay for connection issues, 15 minutes + + pyfalog = Logger(__name__) @unique class PriceStatus(IntEnum): - notFetched = 0 - success = 1 - fail = 2 - notSupported = 3 + initialized = 0 + notSupported = 1 + fetchSuccess = 2 + fetchFail = 3 + fetchTimeout = 4 class Price(object): def __init__(self, typeID): self.typeID = typeID self.time = 0 - self.__price = 0 - self.status = PriceStatus.notFetched + self.price = 0 + self.status = PriceStatus.initialized - @property - def isValid(self): - return self.time >= time.time() - - @property - def price(self): - if self.status != PriceStatus.success: - return 0 + def isValid(self, validityOverride=None): + # Always attempt to update prices which were just initialized, and prices + # of unsupported items (maybe we start supporting them at some point) + if self.status in (PriceStatus.initialized, PriceStatus.notSupported): + return False + elif self.status == PriceStatus.fetchSuccess: + return time() <= self.time + (validityOverride if validityOverride is not None else VALIDITY) + elif self.status == PriceStatus.fetchFail: + return time() <= self.time + REREQUEST + elif self.status == PriceStatus.fetchTimeout: + return time() <= self.time + TIMEOUT else: - return self.__price or 0 + return False - @price.setter - def price(self, price): - self.__price = price + def update(self, status, price=0): + # Keep old price if we failed to fetch new one + if status in (PriceStatus.fetchFail, PriceStatus.fetchTimeout): + price = self.price + elif status != PriceStatus.fetchSuccess: + price = 0 + self.time = time() + self.price = price + self.status = status diff --git a/eos/utils/spoolSupport.py b/eos/utils/spoolSupport.py index de265731f..2014654d4 100644 --- a/eos/utils/spoolSupport.py +++ b/eos/utils/spoolSupport.py @@ -19,21 +19,14 @@ from collections import namedtuple -from enum import IntEnum, unique +from eos.const import SpoolType from eos.utils.float import floatUnerr SpoolOptions = namedtuple('SpoolOptions', ('spoolType', 'spoolAmount', 'force')) -@unique -class SpoolType(IntEnum): - SCALE = 0 # [0..1] - TIME = 1 # Expressed via time in seconds since spool up started - CYCLES = 2 # Expressed in amount of cycles since spool up started - - def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAmount): """ Calculate damage multiplier increment based on passed parameters. Module cycle time diff --git a/eve.db b/eve.db index 6556d4f06..2aba0fb7e 100644 Binary files a/eve.db and b/eve.db differ diff --git a/gui/builtinAdditionPanes/boosterView.py b/gui/builtinAdditionPanes/boosterView.py index 11bac08ca..eda07eb5e 100644 --- a/gui/builtinAdditionPanes/boosterView.py +++ b/gui/builtinAdditionPanes/boosterView.py @@ -110,6 +110,9 @@ class BoosterView(d.Display): self.origional = fit.boosters if fit is not None else None self.boosters = stuff = fit.boosters[:] if fit is not None else None + if stuff is not None: + stuff.sort(key=lambda booster: booster.slot or 0) + if event.fitID != self.lastFitId: self.lastFitId = event.fitID diff --git a/gui/builtinAdditionPanes/cargoView.py b/gui/builtinAdditionPanes/cargoView.py index 5734f622d..29721603c 100644 --- a/gui/builtinAdditionPanes/cargoView.py +++ b/gui/builtinAdditionPanes/cargoView.py @@ -81,7 +81,9 @@ class CargoView(d.Display): if data[0] == "fitting": self.swapModule(x, y, int(data[1])) elif data[0] == "market": - self.mainFrame.command.Submit(cmd.GuiAddCargoCommand(self.mainFrame.getActiveFit(), int(data[1]))) + fit = self.mainFrame.getActiveFit() + if fit: + self.mainFrame.command.Submit(cmd.GuiAddCargoCommand(fit, int(data[1]))) def startDrag(self, event): row = event.GetIndex() diff --git a/gui/builtinAdditionPanes/fighterView.py b/gui/builtinAdditionPanes/fighterView.py index bf797ae15..af98a46ed 100644 --- a/gui/builtinAdditionPanes/fighterView.py +++ b/gui/builtinAdditionPanes/fighterView.py @@ -25,7 +25,7 @@ from gui.builtinMarketBrowser.events import ItemSelected, ITEM_SELECTED import gui.mainFrame import gui.display as d from gui.builtinViewColumns.state import State -from eos.saveddata.module import Slot +from eos.const import FittingSlot from gui.contextMenu import ContextMenu from gui.utils.staticHelpers import DragDropHelper from service.fit import Fit @@ -93,9 +93,9 @@ class FighterView(wx.Panel): if fit: for x in self.labels: if fit.isStructure: - slot = getattr(Slot, "FS_{}".format(x.upper())) + slot = getattr(FittingSlot, "FS_{}".format(x.upper())) else: - slot = getattr(Slot, "F_{}".format(x.upper())) + slot = getattr(FittingSlot, "F_{}".format(x.upper())) used = fit.getSlotsUsed(slot) total = fit.getNumSlots(slot) color = wx.Colour(204, 51, 51) if used > total else wx.SystemSettings.GetColour( @@ -122,8 +122,8 @@ class FighterDisplay(d.Display): # "Max Range", # "Miscellanea", "attr:maxVelocity", - "Fighter Abilities" - # "Price", + "Fighter Abilities", + "Price", ] def __init__(self, parent): diff --git a/gui/builtinAdditionPanes/implantView.py b/gui/builtinAdditionPanes/implantView.py index 0b8521cea..84c437ea3 100644 --- a/gui/builtinAdditionPanes/implantView.py +++ b/gui/builtinAdditionPanes/implantView.py @@ -26,7 +26,7 @@ from gui.builtinViewColumns.state import State from gui.utils.staticHelpers import DragDropHelper from gui.contextMenu import ContextMenu import gui.globalEvents as GE -from eos.saveddata.fit import ImplantLocation +from eos.const import ImplantLocation from service.fit import Fit from service.market import Market import gui.fitCommands as cmd @@ -204,7 +204,10 @@ class ImplantDisplay(d.Display): def removeImplant(self, implant): fitID = self.mainFrame.getActiveFit() - self.mainFrame.command.Submit(cmd.GuiRemoveImplantCommand(fitID, self.original.index(implant))) + sFit = Fit.getInstance() + fit = sFit.getFit(fitID) + if fit.implantLocation == ImplantLocation.FIT: + self.mainFrame.command.Submit(cmd.GuiRemoveImplantCommand(fitID, self.original.index(implant))) def click(self, event): event.Skip() diff --git a/gui/builtinContextMenus/metaSwap.py b/gui/builtinContextMenus/metaSwap.py index 2cbbb2221..d9a34599e 100644 --- a/gui/builtinContextMenus/metaSwap.py +++ b/gui/builtinContextMenus/metaSwap.py @@ -8,6 +8,7 @@ import gui.mainFrame from gui.contextMenu import ContextMenu from service.market import Market from service.settings import ContextMenuSettings +from service.fit import Fit class MetaSwap(ContextMenu): @@ -53,6 +54,8 @@ class MetaSwap(ContextMenu): def getSubMenu(self, context, selection, rootMenu, i, pitem): self.moduleLookup = {} + sFit = Fit.getInstance() + fit = sFit.getFit(self.mainFrame.getActiveFit()) def get_metalevel(x): if "metaLevel" not in x.attributes: @@ -114,6 +117,7 @@ class MetaSwap(ContextMenu): id = ContextMenu.nextID() mitem = wx.MenuItem(rootMenu, id, item.name) + mitem.Enable(fit.canFit(item)) bindmenu.Bind(wx.EVT_MENU, self.handleModule, mitem) self.moduleLookup[id] = item, context diff --git a/gui/builtinContextMenus/moduleAmmoPicker.py b/gui/builtinContextMenus/moduleAmmoPicker.py index f4de653b4..4b4719ecc 100644 --- a/gui/builtinContextMenus/moduleAmmoPicker.py +++ b/gui/builtinContextMenus/moduleAmmoPicker.py @@ -5,7 +5,7 @@ import wx import gui.fitCommands as cmd import gui.mainFrame -from eos.saveddata.module import Hardpoint +from eos.const import FittingHardpoint from gui.bitmap_loader import BitmapLoader from gui.contextMenu import ContextMenu from service.market import Market @@ -136,7 +136,7 @@ class ModuleAmmoPicker(ContextMenu): hardpoint = self.module.hardpoint moduleName = self.module.item.name # Make sure we do not consider mining turrets as combat turrets - if hardpoint == Hardpoint.TURRET and self.module.getModifiedItemAttr("miningAmount", None) is None: + if hardpoint == FittingHardpoint.TURRET and self.module.getModifiedItemAttr("miningAmount", None) is None: self.addSeperator(m, "Long Range") items = [] range_ = None @@ -180,7 +180,7 @@ class ModuleAmmoPicker(ContextMenu): m.Append(item) self.addSeperator(m, "Short Range") - elif hardpoint == Hardpoint.MISSILE and moduleName != 'Festival Launcher': + elif hardpoint == FittingHardpoint.MISSILE and moduleName != 'Festival Launcher': self.charges.sort(key=self.missileSorter) type_ = None sub = None diff --git a/gui/builtinContextMenus/spoolUp.py b/gui/builtinContextMenus/spoolUp.py new file mode 100644 index 000000000..a75568802 --- /dev/null +++ b/gui/builtinContextMenus/spoolUp.py @@ -0,0 +1,82 @@ +# noinspection PyPackageRequirements +import wx + +import eos.config +import gui.mainFrame +from eos.utils.spoolSupport import SpoolType, SpoolOptions +from gui import globalEvents as GE +from gui.contextMenu import ContextMenu +from service.settings import ContextMenuSettings +from service.fit import Fit + + +class SpoolUp(ContextMenu): + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.settings = ContextMenuSettings.getInstance() + self.cycleMap = {} + self.resetId = None + + def display(self, srcContext, selection): + if not self.settings.get('spoolup'): + return False + + if srcContext not in ("fittingModule") or self.mainFrame.getActiveFit() is None: + return False + + self.mod = selection[0] + + return self.mod.item.group.name in ("Precursor Weapon", "Mutadaptive Remote Armor Repairer") + + def getText(self, itmContext, selection): + return "Spoolup Cycles" + + def getSubMenu(self, context, selection, rootMenu, i, pitem): + m = wx.Menu() + if "wxMSW" in wx.PlatformInfo: + bindmenu = rootMenu + else: + bindmenu = m + + isNotDefault = self.mod.spoolType is not None and self.mod.spoolAmount is not None + cycleDefault = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, eos.config.settings['globalDefaultSpoolupPercentage'], True))[0] + cycleCurrent = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, eos.config.settings['globalDefaultSpoolupPercentage'], False))[0] + cycleMin = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True))[0] + cycleMax = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True))[0] + + for cycle in range(cycleMin, cycleMax + 1): + menuId = ContextMenu.nextID() + + # Show default only for current value and when not overriden + if not isNotDefault and cycle == cycleDefault: + text = "{} (default)".format(cycle) + else: + text = "{}".format(cycle) + + item = wx.MenuItem(m, menuId, text, kind=wx.ITEM_CHECK) + bindmenu.Bind(wx.EVT_MENU, self.handleSpoolChange, item) + m.Append(item) + item.Check(isNotDefault and cycle == cycleCurrent) + self.cycleMap[menuId] = cycle + + self.resetId = ContextMenu.nextID() + item = wx.MenuItem(m, self.resetId, "Reset") + bindmenu.Bind(wx.EVT_MENU, self.handleSpoolChange, item) + m.Append(item) + + return m + + def handleSpoolChange(self, event): + if event.Id == self.resetId: + self.mod.spoolType = None + self.mod.spoolAmount = None + elif event.Id in self.cycleMap: + cycles = self.cycleMap[event.Id] + self.mod.spoolType = SpoolType.CYCLES + self.mod.spoolAmount = cycles + fitID = self.mainFrame.getActiveFit() + Fit.getInstance().recalc(fitID) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + + +SpoolUp.register() diff --git a/gui/builtinItemStatsViews/attributeGrouping.py b/gui/builtinItemStatsViews/attributeGrouping.py index de466cf73..6780c176c 100644 --- a/gui/builtinItemStatsViews/attributeGrouping.py +++ b/gui/builtinItemStatsViews/attributeGrouping.py @@ -1,22 +1,4 @@ -from enum import Enum, auto - - -# Define the various groups of attributes -class AttrGroup(Enum): - FITTING = auto() - STRUCTURE = auto() - SHIELD = auto() - ARMOR = auto() - TARGETING = auto() - EWAR_RESISTS = auto() - CAPACITOR = auto() - SHARED_FACILITIES = auto() - FIGHTER_FACILITIES = auto() - ON_DEATH = auto() - JUMP_SYSTEMS = auto() - PROPULSIONS = auto() - FIGHTERS = auto() - +from service.const import GuiAttrGroup RequiredSkillAttrs = sum((["requiredSkill{}".format(x), "requiredSkill{}Level".format(x)] for x in range(1, 7)), []) @@ -45,7 +27,7 @@ for x in AttrGroups: # Start defining all the known attribute groups AttrGroupDict = { - AttrGroup.FITTING : { + GuiAttrGroup.FITTING : { "label" : "Fitting", "attributes": [ # parent-level attributes @@ -67,7 +49,7 @@ AttrGroupDict = { # "mass", ] }, - AttrGroup.STRUCTURE : { + GuiAttrGroup.STRUCTURE : { "label" : "Structure", "attributes": [ "hp", @@ -97,7 +79,7 @@ AttrGroupDict = { "explosiveDamageResonance" ] }, - AttrGroup.ARMOR : { + GuiAttrGroup.ARMOR : { "label": "Armor", "attributes":[ "armorHP", @@ -109,7 +91,7 @@ AttrGroupDict = { ] }, - AttrGroup.SHIELD : { + GuiAttrGroup.SHIELD : { "label": "Shield", "attributes": [ "shieldCapacity", @@ -122,7 +104,7 @@ AttrGroupDict = { ] }, - AttrGroup.EWAR_RESISTS : { + GuiAttrGroup.EWAR_RESISTS : { "label": "Electronic Warfare", "attributes": [ "ECMResistance", @@ -135,14 +117,14 @@ AttrGroupDict = { "weaponDisruptionResistance", ] }, - AttrGroup.CAPACITOR : { + GuiAttrGroup.CAPACITOR : { "label": "Capacitor", "attributes": [ "capacitorCapacity", "rechargeRate", ] }, - AttrGroup.TARGETING : { + GuiAttrGroup.TARGETING : { "label": "Targeting", "attributes": [ "maxTargetRange", @@ -160,7 +142,7 @@ AttrGroupDict = { "scanLadarStrength", ] }, - AttrGroup.SHARED_FACILITIES : { + GuiAttrGroup.SHARED_FACILITIES : { "label" : "Shared Facilities", "attributes": [ "fleetHangarCapacity", @@ -168,7 +150,7 @@ AttrGroupDict = { "maxJumpClones", ] }, - AttrGroup.FIGHTER_FACILITIES: { + GuiAttrGroup.FIGHTER_FACILITIES: { "label": "Fighter Squadron Facilities", "attributes": [ "fighterCapacity", @@ -181,7 +163,7 @@ AttrGroupDict = { "fighterStandupHeavySlots", ] }, - AttrGroup.ON_DEATH : { + GuiAttrGroup.ON_DEATH : { "label": "On Death", "attributes": [ "onDeathDamageEM", @@ -192,7 +174,7 @@ AttrGroupDict = { "onDeathSignatureRadius", ] }, - AttrGroup.JUMP_SYSTEMS : { + GuiAttrGroup.JUMP_SYSTEMS : { "label": "Jump Drive Systems", "attributes": [ "jumpDriveCapacitorNeed", @@ -206,13 +188,13 @@ AttrGroupDict = { "jumpPortalDuration", ] }, - AttrGroup.PROPULSIONS : { + GuiAttrGroup.PROPULSIONS : { "label": "Propulsion", "attributes": [ "maxVelocity" ] }, - AttrGroup.FIGHTERS : { + GuiAttrGroup.FIGHTERS : { "label": "Fighter", "attributes": [ "mass", @@ -225,28 +207,36 @@ AttrGroupDict = { "fighterSquadronOrbitRange", ] }, + GuiAttrGroup.SHIP_GROUP : { + "label" : "Can Fit To", + "attributes": [] + }, } +AttrGroupDict[GuiAttrGroup.SHIP_GROUP]["attributes"].extend([("canFitShipGroup{:02d}".format(i+1), "Group") for i in range(20)]) +AttrGroupDict[GuiAttrGroup.SHIP_GROUP]["attributes"].extend([("canFitShipType{:01d}".format(i+1), "Ship") for i in range(20)]) + Group1 = [ - AttrGroup.FITTING, - AttrGroup.STRUCTURE, - AttrGroup.ARMOR, - AttrGroup.SHIELD, - AttrGroup.EWAR_RESISTS, - AttrGroup.CAPACITOR, - AttrGroup.TARGETING, - AttrGroup.SHARED_FACILITIES, - AttrGroup.FIGHTER_FACILITIES, - AttrGroup.ON_DEATH, - AttrGroup.JUMP_SYSTEMS, - AttrGroup.PROPULSIONS, + GuiAttrGroup.FITTING, + GuiAttrGroup.STRUCTURE, + GuiAttrGroup.ARMOR, + GuiAttrGroup.SHIELD, + GuiAttrGroup.EWAR_RESISTS, + GuiAttrGroup.CAPACITOR, + GuiAttrGroup.TARGETING, + GuiAttrGroup.SHARED_FACILITIES, + GuiAttrGroup.FIGHTER_FACILITIES, + GuiAttrGroup.ON_DEATH, + GuiAttrGroup.JUMP_SYSTEMS, + GuiAttrGroup.PROPULSIONS, + GuiAttrGroup.SHIP_GROUP ] CategoryGroups = { "Fighter" : [ - AttrGroup.FIGHTERS, - AttrGroup.SHIELD, - AttrGroup.TARGETING, + GuiAttrGroup.FIGHTERS, + GuiAttrGroup.SHIELD, + GuiAttrGroup.TARGETING, ], "Ship" : Group1, "Drone" : Group1, diff --git a/gui/builtinItemStatsViews/itemAttributes.py b/gui/builtinItemStatsViews/itemAttributes.py index 747f25066..558762d70 100644 --- a/gui/builtinItemStatsViews/itemAttributes.py +++ b/gui/builtinItemStatsViews/itemAttributes.py @@ -4,12 +4,12 @@ import config # noinspection PyPackageRequirements import wx import wx.lib.agw.hypertreelist -from gui.builtinItemStatsViews.helpers import AutoListCtrl from gui.bitmap_loader import BitmapLoader from gui.utils.numberFormatter import formatAmount, roundDec from enum import IntEnum from gui.builtinItemStatsViews.attributeGrouping import * +from service.const import GuiAttrGroup class AttributeView(IntEnum): @@ -19,7 +19,8 @@ class AttributeView(IntEnum): class ItemParams(wx.Panel): def __init__(self, parent, stuff, item, context=None): - wx.Panel.__init__(self, parent) + # Had to manually set the size here, otherwise column widths couldn't be calculated correctly. See #1878 + wx.Panel.__init__(self, parent, size=(1000, 1000)) mainSizer = wx.BoxSizer(wx.VERTICAL) self.paramList = wx.lib.agw.hypertreelist.HyperTreeList(self, wx.ID_ANY, agwStyle=wx.TR_HIDE_ROOT | wx.TR_NO_LINES | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_HAS_BUTTONS) @@ -28,7 +29,7 @@ class ItemParams(wx.Panel): mainSizer.Add(self.paramList, 1, wx.ALL | wx.EXPAND, 0) self.SetSizer(mainSizer) - self.toggleView = 1 + self.toggleView = AttributeView.NORMAL self.stuff = stuff self.item = item self.attrInfo = {} @@ -40,9 +41,6 @@ class ItemParams(wx.Panel): if self.stuff is not None: self.paramList.AddColumn("Base Value") - self.paramList.SetMainColumn(0) # the one with the tree in it... - self.paramList.SetColumnWidth(0, 300) - self.m_staticline = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) mainSizer.Add(self.m_staticline, 0, wx.EXPAND) bSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -62,6 +60,8 @@ class ItemParams(wx.Panel): mainSizer.Add(bSizer, 0, wx.ALIGN_RIGHT) + self.imageList = wx.ImageList(16, 16) + self.PopulateList() self.toggleViewBtn.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleViewMode) @@ -164,10 +164,24 @@ class ItemParams(wx.Panel): ] ) + def SetupImageList(self): + self.imageList.RemoveAll() + + self.blank_icon = self.imageList.Add(BitmapLoader.getBitmap("transparent16x16", "gui")) + self.unknown_icon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) + + self.paramList.AssignImageList(self.imageList) + def AddAttribute(self, parent, attr): + display = None + + if isinstance(attr, tuple): + display = attr[1] + attr = attr[0] + if attr in self.attrValues and attr not in self.processed_attribs: - data = self.GetData(attr) + data = self.GetData(attr, display) if data is None: return @@ -188,14 +202,14 @@ class ItemParams(wx.Panel): def PopulateList(self): # self.paramList.setResizeColumn(0) - self.imageList = wx.ImageList(16, 16) + self.SetupImageList() self.processed_attribs = set() root = self.paramList.AddRoot("The Root Item") misc_parent = root # We must first deet4ermine if it's categorey already has defined groupings set for it. Otherwise, we default to just using the fitting group - order = CategoryGroups.get(self.item.category.categoryName, [AttrGroup.FITTING]) + order = CategoryGroups.get(self.item.category.categoryName, [GuiAttrGroup.FITTING, GuiAttrGroup.SHIP_GROUP]) # start building out the tree for data in [AttrGroupDict[o] for o in order]: heading = data.get("label") @@ -243,10 +257,14 @@ class ItemParams(wx.Panel): self.AddAttribute(root, name) - self.paramList.AssignImageList(self.imageList) self.Layout() + for i in range(self.paramList.GetMainWindow().GetColumnCount()): + self.paramList.SetColumnWidth(i, wx.LIST_AUTOSIZE) + def GetData(self, attr): + + def GetData(self, attr, displayOveride = None): info = self.attrInfo.get(attr) att = self.attrValues[attr] @@ -264,8 +282,8 @@ class ItemParams(wx.Panel): if self.toggleView == AttributeView.NORMAL and ((attr not in GroupedAttributes and not value) or info is None or not info.published or attr in RequiredSkillAttrs): return None - if info and info.displayName and self.toggleView == 1: - attrName = info.displayName + if info and info.displayName and self.toggleView == AttributeView.NORMAL: + attrName = displayOveride or info.displayName else: attrName = attr @@ -278,27 +296,27 @@ class ItemParams(wx.Panel): icon = BitmapLoader.getBitmap(iconFile, "icons") if icon is None: - icon = BitmapLoader.getBitmap("transparent16x16", "gui") - - attrIcon = self.imageList.Add(icon) + attrIcon = self.blank_icon + else: + attrIcon = self.imageList.Add(icon) else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) + attrIcon = self.unknown_icon else: - attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) + attrIcon = self.unknown_icon # index = self.paramList.AppendItem(root, attrName) # idNameMap[idCount] = attrName # self.paramList.SetPyData(index, idCount) # idCount += 1 - if self.toggleView != 1: + if self.toggleView == AttributeView.RAW: valueUnit = str(value) elif info and info.unit: valueUnit = self.FormatValue(*info.unit.PreformatValue(value)) else: valueUnit = formatAmount(value, 3, 0, 0) - if self.toggleView != 1: + if self.toggleView == AttributeView.RAW: valueUnitDefault = str(valueDefault) elif info and info.unit: valueUnitDefault = self.FormatValue(*info.unit.PreformatValue(valueDefault)) @@ -346,6 +364,7 @@ if __name__ == "__main__": #item = eos.db.getItem(526) # Stasis Webifier I item = eos.db.getItem(486) # 200mm AutoCannon I #item = eos.db.getItem(200) # Phased Plasma L + super().__init__(None, title="Test Attribute Window | {} - {}".format(item.ID, item.name), size=(1000, 500)) if 'wxMSW' in wx.PlatformInfo: @@ -359,6 +378,7 @@ if __name__ == "__main__": main_sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 2) self.SetSizer(main_sizer) + self.Layout() app = wx.App(redirect=False) # Error messages go to popup window top = Frame() diff --git a/gui/builtinItemStatsViews/itemCompare.py b/gui/builtinItemStatsViews/itemCompare.py index 97a4b953d..3c6ad76ed 100644 --- a/gui/builtinItemStatsViews/itemCompare.py +++ b/gui/builtinItemStatsViews/itemCompare.py @@ -16,7 +16,7 @@ class ItemCompare(wx.Panel): def __init__(self, parent, stuff, item, items, context=None): # Start dealing with Price stuff to get that thread going sPrice = ServicePrice.getInstance() - sPrice.getPrices(items, self.UpdateList) + sPrice.getPrices(items, self.UpdateList, fetchTimeout=90) wx.Panel.__init__(self, parent) mainSizer = wx.BoxSizer(wx.VERTICAL) diff --git a/gui/builtinItemStatsViews/itemMutator.py b/gui/builtinItemStatsViews/itemMutator.py index dc91453fa..eb93aedc3 100644 --- a/gui/builtinItemStatsViews/itemMutator.py +++ b/gui/builtinItemStatsViews/itemMutator.py @@ -43,8 +43,12 @@ class ItemMutator(wx.Panel): self.badColor = wx.Colour(255, 64, 0) self.event_mapping = {} + higOverrides = { + ('Stasis Web', 'speedFactor'): False, + } for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName): + highIsGood = higOverrides.get((stuff.item.group.name, m.attribute.name), m.highIsGood) # Format: [raw value, modifier applied to base raw value, display value] range1 = (m.minValue, m.attribute.unit.SimplifyValue(m.minValue)) range2 = (m.maxValue, m.attribute.unit.SimplifyValue(m.maxValue)) @@ -59,7 +63,7 @@ class ItemMutator(wx.Panel): minRange = range2 maxRange = range1 - if (m.highIsGood and minRange[0] >= maxRange[0]) or (not m.highIsGood and minRange[0] <= maxRange[0]): + if (highIsGood and minRange[0] >= maxRange[0]) or (not highIsGood and minRange[0] <= maxRange[0]): betterRange = minRange worseRange = maxRange else: diff --git a/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py b/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py index 7c767c5fb..6ff7c8210 100644 --- a/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py +++ b/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py @@ -36,7 +36,7 @@ class PFContextMenuPref(PreferenceView): self.rbBox1 = wx.RadioBox(panel, -1, "Set as Damage Pattern", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) self.rbBox1.SetSelection(self.settings.get('ammoPattern')) - rbSizerRow1.Add(self.rbBox1, 1, wx.TOP | wx.RIGHT, 5) + rbSizerRow1.Add(self.rbBox1, 1, wx.ALL, 5) self.rbBox1.Bind(wx.EVT_RADIOBOX, self.OnSetting1Change) self.rbBox2 = wx.RadioBox(panel, -1, "Change Skills", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) @@ -56,36 +56,39 @@ class PFContextMenuPref(PreferenceView): self.rbBox4 = wx.RadioBox(panel, -1, "Variations", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) self.rbBox4.SetSelection(self.settings.get('metaSwap')) - rbSizerRow2.Add(self.rbBox4, 1, wx.TOP | wx.RIGHT, 5) + rbSizerRow2.Add(self.rbBox4, 1, wx.ALL, 5) self.rbBox4.Bind(wx.EVT_RADIOBOX, self.OnSetting4Change) - ''' - self.rbBox5 = wx.RadioBox(panel, -1, "Charge", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) - self.rbBox5.SetSelection(self.settings.get('moduleAmmoPicker')) - rbSizerRow2.Add(self.rbBox5, 1, wx.ALL, 5) - self.rbBox5.Bind(wx.EVT_RADIOBOX, self.OnSetting5Change) - ''' + # self.rbBox5 = wx.RadioBox(panel, -1, "Charge", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) + # self.rbBox5.SetSelection(self.settings.get('moduleAmmoPicker')) + # rbSizerRow2.Add(self.rbBox5, 0, wx.ALL, 5) + # self.rbBox5.Bind(wx.EVT_RADIOBOX, self.OnSetting5Change) self.rbBox6 = wx.RadioBox(panel, -1, "Charge (All)", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) self.rbBox6.SetSelection(self.settings.get('moduleGlobalAmmoPicker')) rbSizerRow2.Add(self.rbBox6, 1, wx.ALL, 5) self.rbBox6.Bind(wx.EVT_RADIOBOX, self.OnSetting6Change) + self.rbBox7 = wx.RadioBox(panel, -1, "Project onto Fit", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) + self.rbBox7.SetSelection(self.settings.get('project')) + rbSizerRow2.Add(self.rbBox7, 1, wx.ALL, 5) + self.rbBox7.Bind(wx.EVT_RADIOBOX, self.OnSetting7Change) + mainSizer.Add(rbSizerRow2, 1, wx.ALL | wx.EXPAND, 0) # Row 3 rbSizerRow3 = wx.BoxSizer(wx.HORIZONTAL) - self.rbBox7 = wx.RadioBox(panel, -1, "Project onto Fit", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) - self.rbBox7.SetSelection(self.settings.get('project')) - rbSizerRow3.Add(self.rbBox7, 1, wx.TOP | wx.RIGHT, 5) - self.rbBox7.Bind(wx.EVT_RADIOBOX, self.OnSetting7Change) - self.rbBox8 = wx.RadioBox(panel, -1, "Fill with module", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) self.rbBox8.SetSelection(self.settings.get('moduleFill')) - rbSizerRow3.Add(self.rbBox8, 1, wx.TOP | wx.RIGHT, 5) + rbSizerRow3.Add(self.rbBox8, 1, wx.ALL, 5) self.rbBox8.Bind(wx.EVT_RADIOBOX, self.OnSetting8Change) + self.rbBox9 = wx.RadioBox(panel, -1, "Spoolup", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) + self.rbBox9.SetSelection(self.settings.get('spoolup')) + rbSizerRow3.Add(self.rbBox9, 1, wx.ALL, 5) + self.rbBox9.Bind(wx.EVT_RADIOBOX, self.OnSetting9Change) + mainSizer.Add(rbSizerRow3, 1, wx.ALL | wx.EXPAND, 0) panel.SetSizer(mainSizer) @@ -115,6 +118,9 @@ class PFContextMenuPref(PreferenceView): def OnSetting8Change(self, event): self.settings.set('moduleFill', event.GetInt()) + def OnSetting9Change(self, event): + self.settings.set('spoolup', event.GetInt()) + def getImage(self): return BitmapLoader.getBitmap("settings_menu", "gui") diff --git a/gui/builtinPreferenceViews/pyfaEnginePreferences.py b/gui/builtinPreferenceViews/pyfaEnginePreferences.py index c41921393..fe5bcc5cb 100644 --- a/gui/builtinPreferenceViews/pyfaEnginePreferences.py +++ b/gui/builtinPreferenceViews/pyfaEnginePreferences.py @@ -8,6 +8,7 @@ import gui.globalEvents as GE from gui.preferenceView import PreferenceView from service.settings import EOSSettings import gui.mainFrame +from wx.lib.intctrl import IntCtrl logger = logging.getLogger(__name__) @@ -61,6 +62,22 @@ class PFFittingEnginePref(PreferenceView): wx.DefaultPosition, wx.DefaultSize, 0) mainSizer.Add(self.cbUniversalAdaptiveArmorHardener, 0, wx.ALL | wx.EXPAND, 5) + + spoolup_sizer = wx.BoxSizer(wx.HORIZONTAL) + + self.spool_up_label = wx.StaticText(panel, wx.ID_ANY, "Global Default Spoolup Percentage:", wx.DefaultPosition, wx.DefaultSize, 0) + self.spool_up_label.Wrap(-1) + self.spool_up_label.SetCursor(helpCursor) + self.spool_up_label.SetToolTip( + wx.ToolTip('The amount of spoolup to use by default on module which support it. Can be changed on a per-module basis')) + + spoolup_sizer.Add(self.spool_up_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + self.spoolup_value = IntCtrl(panel, min=0, max=100, limited=True) + spoolup_sizer.Add(self.spoolup_value , 0, wx.ALL, 5) + + mainSizer.Add(spoolup_sizer, 0, wx.ALL | wx.EXPAND, 0) + # Future code once new cap sim is implemented ''' self.cbGlobalForceReactivationTimer = wx.CheckBox( panel, wx.ID_ANY, u"Factor in reactivation timer", wx.DefaultPosition, wx.DefaultSize, 0 ) @@ -96,9 +113,15 @@ class PFFittingEnginePref(PreferenceView): self.cbUniversalAdaptiveArmorHardener.SetValue(self.engine_settings.get("useStaticAdaptiveArmorHardener")) self.cbUniversalAdaptiveArmorHardener.Bind(wx.EVT_CHECKBOX, self.OnCBUniversalAdaptiveArmorHardenerChange) + self.spoolup_value.SetValue(int(self.engine_settings.get("globalDefaultSpoolupPercentage") * 100)) + self.spoolup_value.Bind(wx.lib.intctrl.EVT_INT, self.OnSpoolupChange) + panel.SetSizer(mainSizer) panel.Layout() + def OnSpoolupChange(self, event): + self.engine_settings.set("globalDefaultSpoolupPercentage", self.spoolup_value.GetValue() / 100) + def OnCBGlobalForceReloadStateChange(self, event): self.sFit.serviceFittingOptions["useGlobalForceReload"] = self.cbGlobalForceReload.GetValue() fitID = self.mainFrame.getActiveFit() diff --git a/gui/builtinStatsViews/firepowerViewFull.py b/gui/builtinStatsViews/firepowerViewFull.py index 4c773c9f1..c678e0b61 100644 --- a/gui/builtinStatsViews/firepowerViewFull.py +++ b/gui/builtinStatsViews/firepowerViewFull.py @@ -25,7 +25,7 @@ from gui.bitmap_loader import BitmapLoader from gui.utils.numberFormatter import formatAmount, roundToPrec from eos.utils.spoolSupport import SpoolType, SpoolOptions from service.fit import Fit - +import eos.config class FirepowerViewFull(StatsView): name = "firepowerViewFull" @@ -157,8 +157,7 @@ class FirepowerViewFull(StatsView): formatAmount(preSpool, prec, lowest, highest), formatAmount(fullSpool, prec, lowest, highest)) - # TODO: fetch spoolup option - defaultSpoolValue = 1 + defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] stats = ( ( "labelFullDpsWeapon", diff --git a/gui/builtinStatsViews/outgoingViewFull.py b/gui/builtinStatsViews/outgoingViewFull.py index 3b6a6ce58..740042b8c 100644 --- a/gui/builtinStatsViews/outgoingViewFull.py +++ b/gui/builtinStatsViews/outgoingViewFull.py @@ -23,6 +23,7 @@ from gui.statsView import StatsView from gui.bitmap_loader import BitmapLoader from gui.utils.numberFormatter import formatAmount, roundToPrec from eos.utils.spoolSupport import SpoolType, SpoolOptions +import eos.config stats = [ @@ -101,8 +102,7 @@ class OutgoingViewFull(StatsView): formatAmount(preSpool, prec, lowest, highest), formatAmount(fullSpool, prec, lowest, highest)) - # TODO: fetch spoolup option - defaultSpoolValue = 1 + defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] counter = 0 for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats: label = getattr(self, labelName) diff --git a/gui/builtinStatsViews/outgoingViewMinimal.py b/gui/builtinStatsViews/outgoingViewMinimal.py index 055a65e9e..4ed86831f 100644 --- a/gui/builtinStatsViews/outgoingViewMinimal.py +++ b/gui/builtinStatsViews/outgoingViewMinimal.py @@ -22,6 +22,7 @@ import wx from gui.statsView import StatsView from gui.utils.numberFormatter import formatAmount, roundToPrec from eos.utils.spoolSupport import SpoolType, SpoolOptions +import eos.config stats = [ @@ -100,8 +101,7 @@ class OutgoingViewMinimal(StatsView): formatAmount(preSpool, prec, lowest, highest), formatAmount(fullSpool, prec, lowest, highest)) - # TODO: fetch spoolup option - defaultSpoolValue = 1 + defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] counter = 0 for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats: label = getattr(self, labelName) diff --git a/gui/builtinStatsViews/priceViewFull.py b/gui/builtinStatsViews/priceViewFull.py index 767ad958f..92aea617f 100644 --- a/gui/builtinStatsViews/priceViewFull.py +++ b/gui/builtinStatsViews/priceViewFull.py @@ -22,7 +22,7 @@ import wx from gui.statsView import StatsView from gui.bitmap_loader import BitmapLoader from gui.utils.numberFormatter import formatAmount -from service.price import Price +from service.price import Fit, Price from service.settings import PriceMenuSettings @@ -51,7 +51,7 @@ class PriceViewFull(StatsView): gridPrice = wx.GridSizer(2, 3, 0, 0) contentSizer.Add(gridPrice, 0, wx.EXPAND | wx.ALL, 0) - for _type in ("ship", "fittings", "total", "drones", "cargoBay", "character"): + for _type in ("ship", "fittings", "character", "drones", "cargoBay", "total"): if _type in "ship": image = "ship_big" elif _type in ("fittings", "total"): @@ -79,11 +79,8 @@ class PriceViewFull(StatsView): def refreshPanel(self, fit): if fit is not None: self.fit = fit - - fit_items = Price.fitItemsList(fit) - - sPrice = Price.getInstance() - sPrice.getPrices(fit_items, self.processPrices) + fit_items = set(Fit.fitItemIter(fit)) + Price.getInstance().getPrices(fit_items, self.processPrices, fetchTimeout=30) self.labelEMStatus.SetLabel("Updating prices...") self.refreshPanelPrices(fit) diff --git a/gui/builtinStatsViews/priceViewMinimal.py b/gui/builtinStatsViews/priceViewMinimal.py index 8d6a5ce83..4506f2686 100644 --- a/gui/builtinStatsViews/priceViewMinimal.py +++ b/gui/builtinStatsViews/priceViewMinimal.py @@ -22,7 +22,7 @@ import wx from gui.statsView import StatsView from gui.bitmap_loader import BitmapLoader from gui.utils.numberFormatter import formatAmount -from service.price import Price +from service.price import Fit, Price from service.settings import PriceMenuSettings @@ -73,11 +73,8 @@ class PriceViewMinimal(StatsView): def refreshPanel(self, fit): if fit is not None: self.fit = fit - - fit_items = Price.fitItemsList(fit) - - sPrice = Price.getInstance() - sPrice.getPrices(fit_items, self.processPrices) + fit_items = set(Fit.fitItemIter(fit)) + Price.getInstance().getPrices(fit_items, self.processPrices, fetchTimeout=30) self.labelEMStatus.SetLabel("Updating prices...") self.refreshPanelPrices(fit) diff --git a/gui/builtinStatsViews/resourcesViewFull.py b/gui/builtinStatsViews/resourcesViewFull.py index 77a1eaae5..37675a202 100644 --- a/gui/builtinStatsViews/resourcesViewFull.py +++ b/gui/builtinStatsViews/resourcesViewFull.py @@ -26,7 +26,7 @@ import gui.mainFrame from gui.chrome_tabs import EVT_NOTEBOOK_PAGE_CHANGED from gui.utils import fonts -from eos.saveddata.module import Hardpoint +from eos.const import FittingHardpoint from gui.utils.numberFormatter import formatAmount @@ -196,9 +196,9 @@ class ResourcesViewFull(StatsView): # If we did anything intresting, we'd update our labels to reflect the new fit's stats here stats = ( - ("label%sUsedTurretHardpoints", lambda: fit.getHardpointsUsed(Hardpoint.TURRET), 0, 0, 0), + ("label%sUsedTurretHardpoints", lambda: fit.getHardpointsUsed(FittingHardpoint.TURRET), 0, 0, 0), ("label%sTotalTurretHardpoints", lambda: fit.ship.getModifiedItemAttr('turretSlotsLeft'), 0, 0, 0), - ("label%sUsedLauncherHardpoints", lambda: fit.getHardpointsUsed(Hardpoint.MISSILE), 0, 0, 0), + ("label%sUsedLauncherHardpoints", lambda: fit.getHardpointsUsed(FittingHardpoint.MISSILE), 0, 0, 0), ("label%sTotalLauncherHardpoints", lambda: fit.ship.getModifiedItemAttr('launcherSlotsLeft'), 0, 0, 0), ("label%sUsedDronesActive", lambda: fit.activeDrones, 0, 0, 0), ("label%sTotalDronesActive", lambda: fit.extraAttributes["maxActiveDrones"], 0, 0, 0), @@ -278,12 +278,16 @@ class ResourcesViewFull(StatsView): totalCalibrationPoints = value labelTCP = label + # See #1877 + shown = label.Shown + label.Show(True) if isinstance(value, str): label.SetLabel(value) label.SetToolTip(wx.ToolTip(value)) else: label.SetLabel(formatAmount(value, prec, lowest, highest)) label.SetToolTip(wx.ToolTip("%.1f" % value)) + label.Show(shown) colorWarn = wx.Colour(204, 51, 51) colorNormal = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) diff --git a/gui/builtinViewColumns/baseIcon.py b/gui/builtinViewColumns/baseIcon.py index 207d639c5..e5cbd62f2 100644 --- a/gui/builtinViewColumns/baseIcon.py +++ b/gui/builtinViewColumns/baseIcon.py @@ -2,8 +2,9 @@ import wx from eos.saveddata.implant import Implant from eos.saveddata.drone import Drone -from eos.saveddata.module import Module, Slot, Rack +from eos.saveddata.module import Module, Rack from eos.saveddata.fit import Fit +from eos.const import FittingSlot from gui.viewColumn import ViewColumn @@ -32,7 +33,7 @@ class BaseIcon(ViewColumn): return self.shipImage elif isinstance(stuff, Module): if stuff.isEmpty: - return self.fittingView.imageList.GetImageIndex("slot_%s_small" % Slot.getName(stuff.slot).lower(), + return self.fittingView.imageList.GetImageIndex("slot_%s_small" % FittingSlot(stuff.slot).name.lower(), "gui") else: return self.loadIconFile(stuff.item.iconID or "") diff --git a/gui/builtinViewColumns/baseName.py b/gui/builtinViewColumns/baseName.py index 368dd74b6..cad1eebd0 100644 --- a/gui/builtinViewColumns/baseName.py +++ b/gui/builtinViewColumns/baseName.py @@ -25,8 +25,9 @@ from eos.saveddata.cargo import Cargo from eos.saveddata.implant import Implant from eos.saveddata.drone import Drone from eos.saveddata.fighter import Fighter -from eos.saveddata.module import Module, Slot, Rack +from eos.saveddata.module import Module, Rack from eos.saveddata.fit import Fit +from eos.const import FittingSlot from service.fit import Fit as FitSvc from service.market import Market from gui.viewColumn import ViewColumn @@ -72,10 +73,10 @@ class BaseName(ViewColumn): return "%s (%s)" % (stuff.name, stuff.ship.item.name) elif isinstance(stuff, Rack): if FitSvc.getInstance().serviceFittingOptions["rackLabels"]: - if stuff.slot == Slot.MODE: + if stuff.slot == FittingSlot.MODE: return '─ Tactical Mode ─' else: - return '─ {} {} Slot{}─'.format(stuff.num, Slot.getName(stuff.slot).capitalize(), '' if stuff.num == 1 else 's') + return '─ {} {} Slot{}─'.format(stuff.num, FittingSlot(stuff.slot).name.capitalize(), '' if stuff.num == 1 else 's') else: return "" elif isinstance(stuff, Module): @@ -89,7 +90,7 @@ class BaseName(ViewColumn): return "{} {}".format(type.name, stuff.item.name[-1:]) if stuff.isEmpty: - return "%s Slot" % Slot.getName(stuff.slot).capitalize() + return "%s Slot" % FittingSlot(stuff.slot).name.capitalize() else: return stuff.item.name elif isinstance(stuff, Implant): diff --git a/gui/builtinViewColumns/misc.py b/gui/builtinViewColumns/misc.py index e9bff3d56..24c5d7a55 100644 --- a/gui/builtinViewColumns/misc.py +++ b/gui/builtinViewColumns/misc.py @@ -28,6 +28,7 @@ from gui.bitmap_loader import BitmapLoader from gui.utils.numberFormatter import formatAmount from gui.utils.listFormatter import formatList from eos.utils.spoolSupport import SpoolType, SpoolOptions +import eos.config class Miscellanea(ViewColumn): @@ -117,8 +118,8 @@ class Miscellanea(ViewColumn): text = "{0}".format(formatAmount(trackingSpeed, 3, 0, 3)) tooltip = "tracking speed" info.append((text, tooltip)) - # TODO: fetch spoolup option - defaultSpoolValue = 1 + + defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] spoolTime = stuff.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))[1] if spoolTime: text = "{0}s".format(formatAmount(spoolTime, 3, 0, 3)) @@ -339,8 +340,7 @@ class Miscellanea(ViewColumn): tooltip = "Armor repaired per second" return text, tooltip elif itemGroup == "Mutadaptive Remote Armor Repairer": - # TODO: fetch spoolup option - defaultSpoolValue = 1 + defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] spoolOptDefault = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False) spoolOptPre = SpoolOptions(SpoolType.SCALE, 0, True) spoolOptFull = SpoolOptions(SpoolType.SCALE, 1, True) diff --git a/gui/builtinViewColumns/price.py b/gui/builtinViewColumns/price.py index 56901c172..563d0b1c0 100644 --- a/gui/builtinViewColumns/price.py +++ b/gui/builtinViewColumns/price.py @@ -22,6 +22,7 @@ import wx from eos.saveddata.cargo import Cargo from eos.saveddata.drone import Drone +from eos.saveddata.fighter import Fighter from eos.saveddata.price import PriceStatus from service.price import Price as ServicePrice from gui.viewColumn import ViewColumn @@ -29,6 +30,20 @@ from gui.bitmap_loader import BitmapLoader from gui.utils.numberFormatter import formatAmount +def formatPrice(stuff, priceObj): + textItems = [] + if priceObj.price: + mult = 1 + if isinstance(stuff, (Drone, Cargo)): + mult = stuff.amount + elif isinstance(stuff, Fighter): + mult = stuff.amountActive + textItems.append(formatAmount(priceObj.price * mult, 3, 3, 9, currency=True)) + if priceObj.status in (PriceStatus.fetchFail, PriceStatus.fetchTimeout): + textItems.append("(!)") + return " ".join(textItems) + + class Price(ViewColumn): name = "Price" @@ -48,35 +63,21 @@ class Price(ViewColumn): priceObj = stuff.item.price - if not priceObj.isValid: + if not priceObj.isValid(): return False - # Fetch actual price as float to not modify its value on Price object - price = priceObj.price - - if price == 0: - return "" - - if isinstance(stuff, Drone) or isinstance(stuff, Cargo): - price *= stuff.amount - - return formatAmount(price, 3, 3, 9, currency=True) + return formatPrice(stuff, priceObj) def delayedText(self, mod, display, colItem): sPrice = ServicePrice.getInstance() def callback(item): - price = item[0] - textItems = [] - if price.price: - textItems.append(formatAmount(price.price, 3, 3, 9, currency=True)) - if price.status == PriceStatus.fail: - textItems.append("(!)") - colItem.SetText(" ".join(textItems)) + priceObj = item[0] + colItem.SetText(formatPrice(mod, priceObj)) display.SetItem(colItem) - sPrice.getPrices([mod.item], callback, True) + sPrice.getPrices([mod.item], callback, waitforthread=True) def getImageId(self, mod): return -1 diff --git a/gui/builtinViewColumns/state.py b/gui/builtinViewColumns/state.py index c99ae2947..d62aed631 100644 --- a/gui/builtinViewColumns/state.py +++ b/gui/builtinViewColumns/state.py @@ -24,7 +24,8 @@ import wx from eos.saveddata.fit import Fit from eos.saveddata.implant import Implant from eos.saveddata.drone import Drone -from eos.saveddata.module import Module, State as State_, Rack +from eos.saveddata.module import Module, Rack +from eos.const import FittingModuleState as State_ from gui.viewColumn import ViewColumn import gui.mainFrame @@ -46,12 +47,11 @@ class State(ViewColumn): def getToolTip(self, mod): if isinstance(mod, Module) and not mod.isEmpty: - return State_.getName(mod.state).title() + return State_(mod.state).name.title() def getImageId(self, stuff): - generic_active = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(1).lower(), "gui") - generic_inactive = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(-1).lower(), - "gui") + generic_active = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.ACTIVE.name.lower(), "gui") + generic_inactive = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.OFFLINE.name.lower(), "gui") if isinstance(stuff, Drone): if stuff.amountActive > 0: @@ -64,7 +64,7 @@ class State(ViewColumn): if stuff.isEmpty: return -1 else: - return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(stuff.state).lower(), + return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_(stuff.state).name.lower(), "gui") elif isinstance(stuff, Fit): fitID = self.mainFrame.getActiveFit() @@ -83,7 +83,7 @@ class State(ViewColumn): return generic_inactive elif isinstance(stuff, Implant) and stuff.character: # if we're showing character implants, show an "online" state, which should not be changed - return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(0).lower(), "gui") + return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.ONLINE.name.lower(), "gui") else: active = getattr(stuff, "active", None) if active is None: diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index 372ea6a2e..97ba7d8f6 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -30,7 +30,8 @@ import gui.globalEvents as GE import gui.mainFrame import gui.multiSwitch from eos.saveddata.mode import Mode -from eos.saveddata.module import Module, Rack, Slot +from eos.saveddata.module import Module, Rack +from eos.const import FittingSlot from gui.bitmap_loader import BitmapLoader from gui.builtinMarketBrowser.events import ITEM_SELECTED from gui.builtinShipBrowser.events import EVT_FIT_REMOVED, EVT_FIT_RENAMED, EVT_FIT_SELECTED, FitSelected @@ -474,7 +475,14 @@ class FittingView(d.Display): sFit = Fit.getInstance() fit = sFit.getFit(self.activeFitID) - slotOrder = [Slot.SUBSYSTEM, Slot.HIGH, Slot.MED, Slot.LOW, Slot.RIG, Slot.SERVICE] + slotOrder = [ + FittingSlot.SUBSYSTEM, + FittingSlot.HIGH, + FittingSlot.MED, + FittingSlot.LOW, + FittingSlot.RIG, + FittingSlot.SERVICE + ] if fit is not None: self.mods = fit.modules[:] @@ -507,7 +515,7 @@ class FittingView(d.Display): # while also marking the mode header position in the Blanks list if sFit.serviceFittingOptions["rackSlots"]: self.blanks.append(len(self.mods)) - self.mods.append(Rack.buildRack(Slot.MODE, None)) + self.mods.append(Rack.buildRack(FittingSlot.MODE, None)) self.mods.append(fit.mode) else: @@ -648,8 +656,7 @@ class FittingView(d.Display): slotMap = {} # test for too many modules (happens with t3s / CCP change in slot layout) - for slotType in Slot.getTypes(): - slot = Slot.getValue(slotType) + for slot in [e.value for e in FittingSlot]: slotMap[slot] = fit.getSlotsFree(slot) < 0 for i, mod in enumerate(self.mods): @@ -735,8 +742,8 @@ class FittingView(d.Display): return slotMap = {} - for slotType in Slot.getTypes(): - slot = Slot.getValue(slotType) + + for slot in [e.value for e in FittingSlot]: slotMap[slot] = fit.getSlotsFree(slot) < 0 padding = 2 diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 2b4d59341..60dbcd3e5 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -150,7 +150,7 @@ class CharacterEntityEditor(EntityEditor): class CharacterEditor(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="pyfa: Character Editor", pos=wx.DefaultPosition, - size=wx.Size(640, 600), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER) + size=wx.Size(640, 600), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER | wx.FRAME_FLOAT_ON_PARENT) i = wx.Icon(BitmapLoader.getBitmap("character_small", "gui")) self.SetIcon(i) @@ -353,7 +353,7 @@ class SkillTreeView(wx.Panel): self.skillBookDirtyImageId = self.imageList.Add(wx.Icon(BitmapLoader.getBitmap("skill_small_red", "gui"))) tree.AppendColumn("Skill") - tree.AppendColumn("Level") + tree.AppendColumn("Level", align=wx.ALIGN_CENTER) # tree.SetMainColumn(0) self.root = tree.GetRootItem() @@ -361,13 +361,17 @@ class SkillTreeView(wx.Panel): # # tree.SetItemText(self.root, 1, "Levels") - # tree.SetColumnWidth(0, 300) + # first one doesn't work right in Windows. Second one doesn't work right in GTK. Together, we make sure it works. + # Gotta love wx + tree.SetColumnWidth(0, 525) + tree.SetColumnWidth(1, 100) self.btnSecStatus = wx.Button(self, wx.ID_ANY, "Sec Status: {0:.2f}".format(char.secStatus or 0.0)) self.btnSecStatus.Bind(wx.EVT_BUTTON, self.onSecStatus) self.populateSkillTree() + tree.Bind(wx.dataview.EVT_TREELIST_ITEM_ACTIVATED, self.expand) tree.Bind(wx.dataview.EVT_TREELIST_ITEM_EXPANDING, self.expandLookup) tree.Bind(wx.dataview.EVT_TREELIST_ITEM_CONTEXT_MENU, self.scheduleMenu) @@ -427,7 +431,8 @@ class SkillTreeView(wx.Panel): self.levelChangeMenu.Bind(wx.EVT_MENU, self.changeLevel) self.SetSizer(pmainSizer) - self.Layout() + # This cuases issues with GTK, see #1866 + # self.Layout() def importSkills(self, evt): @@ -554,9 +559,18 @@ class SkillTreeView(wx.Panel): if event: event.Skip() + def expand(self, event): + root = event.GetItem() + tree = self.skillTreeListCtrl + if tree.IsExpanded(root): + tree.Collapse(root) + else: + tree.Expand(root) + def expandLookup(self, event): root = event.GetItem() tree = self.skillTreeListCtrl + child = tree.GetFirstChild(root) if tree.GetItemText(child) == "dummy": tree.DeleteItem(child) diff --git a/gui/contextMenu.py b/gui/contextMenu.py index 7c1caf085..b479ab913 100644 --- a/gui/contextMenu.py +++ b/gui/contextMenu.py @@ -183,6 +183,7 @@ from gui.builtinContextMenus import ( # noqa: E402,F401 openFit, moduleGlobalAmmoPicker, moduleAmmoPicker, + spoolUp, itemStats, damagePattern, marketJump, diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 91725e048..985b98d01 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -26,6 +26,10 @@ import wx from service.port.eft import EFT_OPTIONS from service.port.multibuy import MULTIBUY_OPTIONS from service.settings import SettingsProvider +from service.port import EfsPort, Port +from service.const import PortMultiBuyOptions +from eos.db import getFit +from gui.utils.clipboard import toClipboard class CopySelectDialog(wx.Dialog): @@ -39,6 +43,17 @@ class CopySelectDialog(wx.Dialog): 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) + + self.CopySelectDict = { + CopySelectDialog.copyFormatEft : self.exportEft, + CopySelectDialog.copyFormatXml : self.exportXml, + CopySelectDialog.copyFormatDna : self.exportDna, + CopySelectDialog.copyFormatEsi : self.exportEsi, + CopySelectDialog.copyFormatMultiBuy: self.exportMultiBuy, + CopySelectDialog.copyFormatEfs : self.exportEfs + } + + self.mainFrame = parent mainSizer = wx.BoxSizer(wx.VERTICAL) self.copyFormats = OrderedDict(( @@ -87,8 +102,8 @@ class CopySelectDialog(wx.Dialog): self.options[formatId][optId] = checkbox if self.settings['options'].get(formatId, {}).get(optId, defaultFormatOptions.get(formatId, {}).get(optId)): checkbox.SetValue(True) - bsizer.Add(checkbox, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) - mainSizer.Add(bsizer, 1, wx.EXPAND | wx.LEFT, 20) + bsizer.Add(checkbox, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) + mainSizer.Add(bsizer, 0, wx.EXPAND | wx.LEFT, 20) buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL) if buttonSizer: @@ -99,6 +114,31 @@ class CopySelectDialog(wx.Dialog): self.Fit() self.Center() + def Validate(self): + # Since this dialog is shown through aa ShowModal(), we hook into the Validate function to veto the closing of the dialog until we're ready. + # This always returns False, and when we're ready will EndModal() + selected = self.GetSelected() + options = self.GetOptions() + + settings = SettingsProvider.getInstance().getSettings("pyfaExport") + settings["format"] = selected + settings["options"] = options + self.waitDialog = None + + def cb(text): + if self.waitDialog: + del self.waitDialog + toClipboard(text) + self.EndModal(wx.ID_OK) + + export_options = options.get(selected) + if selected == CopySelectDialog.copyFormatMultiBuy and export_options.get(PortMultiBuyOptions.OPTIMIZE_PRICES, False): + self.waitDialog = wx.BusyInfo("Optimizing Prices", parent=self) + + self.CopySelectDict[selected](export_options, callback=cb) + + return False + def Selected(self, event): obj = event.GetEventObject() formatName = obj.GetLabel() @@ -119,3 +159,27 @@ class CopySelectDialog(wx.Dialog): for formatId in self.options: options[formatId] = {optId: ch.IsChecked() for optId, ch in self.options[formatId].items()} return options + + def exportEft(self, options, callback): + fit = getFit(self.mainFrame.getActiveFit()) + Port.exportEft(fit, options, callback) + + def exportDna(self, options, callback): + fit = getFit(self.mainFrame.getActiveFit()) + Port.exportDna(fit, callback) + + def exportEsi(self, options, callback): + fit = getFit(self.mainFrame.getActiveFit()) + Port.exportESI(fit, callback) + + def exportXml(self, options, callback): + fit = getFit(self.mainFrame.getActiveFit()) + Port.exportXml(None, fit, callback) + + def exportMultiBuy(self, options, callback): + fit = getFit(self.mainFrame.getActiveFit()) + Port.exportMultiBuy(fit, options, callback) + + def exportEfs(self, options, callback): + fit = getFit(self.mainFrame.getActiveFit()) + EfsPort.exportEfs(fit, 0, callback) \ No newline at end of file diff --git a/gui/fitCommands/__init__.py b/gui/fitCommands/__init__.py index 42b718c5d..83b7dbb46 100644 --- a/gui/fitCommands/__init__.py +++ b/gui/fitCommands/__init__.py @@ -35,3 +35,5 @@ from .guiToggleDrone import GuiToggleDroneCommand from .guiFitRename import GuiFitRenameCommand from .guiChangeImplantLocation import GuiChangeImplantLocation from .guiImportMutatedModule import GuiImportMutatedModuleCommand +from .guiSetSpoolup import GuiSetSpoolup +from .guiRebaseItems import GuiRebaseItemsCommand diff --git a/gui/fitCommands/calc/fitAddModule.py b/gui/fitCommands/calc/fitAddModule.py index cd96b6d4f..75611de28 100644 --- a/gui/fitCommands/calc/fitAddModule.py +++ b/gui/fitCommands/calc/fitAddModule.py @@ -1,5 +1,6 @@ import wx -from eos.saveddata.module import Module, State +from eos.saveddata.module import Module +from eos.const import FittingModuleState import eos.db from logbook import Logger from service.fit import Fit @@ -52,8 +53,8 @@ class FitAddModuleCommand(wx.Command): self.module.owner = fit numSlots = len(fit.modules) fit.modules.append(self.module) - if self.module.isValidState(State.ACTIVE): - self.module.state = State.ACTIVE + if self.module.isValidState(FittingModuleState.ACTIVE): + self.module.state = FittingModuleState.ACTIVE # todo: fix these # As some items may affect state-limiting attributes of the ship, calculate new attributes first diff --git a/gui/fitCommands/calc/fitAddProjectedEnv.py b/gui/fitCommands/calc/fitAddProjectedEnv.py index dc1a0443a..87d1bd890 100644 --- a/gui/fitCommands/calc/fitAddProjectedEnv.py +++ b/gui/fitCommands/calc/fitAddProjectedEnv.py @@ -1,5 +1,6 @@ import wx -from eos.saveddata.module import Module, State +from eos.saveddata.module import Module +from eos.const import FittingModuleState import eos.db from logbook import Logger pyfalog = Logger(__name__) @@ -28,7 +29,7 @@ class FitAddProjectedEnvCommand(wx.Command): # todo: thing to check for existing environmental effects - module.state = State.ONLINE + module.state = FittingModuleState.ONLINE if module.isExclusiveSystemEffect: # if this is an exclusive system effect, we need to cache the old one. We make room for the new one here, which returns the old one self.old_item = fit.projectedModules.makeRoom(module) diff --git a/gui/fitCommands/calc/fitAddProjectedModule.py b/gui/fitCommands/calc/fitAddProjectedModule.py index 6d2ad3f4f..089705ba1 100644 --- a/gui/fitCommands/calc/fitAddProjectedModule.py +++ b/gui/fitCommands/calc/fitAddProjectedModule.py @@ -1,7 +1,8 @@ import wx import eos.db from logbook import Logger -from eos.saveddata.module import Module, State +from eos.saveddata.module import Module +from eos.const import FittingModuleState pyfalog = Logger(__name__) @@ -27,9 +28,9 @@ class FitAddProjectedModuleCommand(wx.Command): except ValueError: return False - module.state = State.ACTIVE + module.state = FittingModuleState.ACTIVE if not module.canHaveState(module.state, fit): - module.state = State.OFFLINE + module.state = FittingModuleState.OFFLINE fit.projectedModules.append(module) eos.db.commit() diff --git a/gui/fitCommands/calc/fitImportMutatedModule.py b/gui/fitCommands/calc/fitImportMutatedModule.py index 3a28c68ea..bae0ef0d8 100644 --- a/gui/fitCommands/calc/fitImportMutatedModule.py +++ b/gui/fitCommands/calc/fitImportMutatedModule.py @@ -1,5 +1,6 @@ import wx -from eos.saveddata.module import Module, State +from eos.saveddata.module import Module +from eos.const import FittingModuleState import eos.db from eos.db.gamedata.queries import getDynamicItem from logbook import Logger @@ -62,8 +63,8 @@ class FitImportMutatedCommand(wx.Command): module.owner = fit numSlots = len(fit.modules) fit.modules.append(module) - if module.isValidState(State.ACTIVE): - module.state = State.ACTIVE + if module.isValidState(FittingModuleState.ACTIVE): + module.state = FittingModuleState.ACTIVE # todo: fix these # As some items may affect state-limiting attributes of the ship, calculate new attributes first diff --git a/gui/fitCommands/calc/fitRebaseItem.py b/gui/fitCommands/calc/fitRebaseItem.py new file mode 100644 index 000000000..1fb44b266 --- /dev/null +++ b/gui/fitCommands/calc/fitRebaseItem.py @@ -0,0 +1,31 @@ +import wx + +from logbook import Logger + +import eos.db + + +pyfalog = Logger(__name__) + + +class FitRebaseItemCommand(wx.Command): + + def __init__(self, fitID, containerName, position, newTypeID): + wx.Command.__init__(self, True, "Rebase Item") + self.fitID = fitID + self.containerName = containerName + self.position = position + self.newTypeID = newTypeID + self.oldTypeID = None + + def Do(self): + fit = eos.db.getFit(self.fitID) + obj = getattr(fit, self.containerName)[self.position] + self.oldTypeID = getattr(obj.item, "ID", None) + newItem = eos.db.getItem(self.newTypeID) + obj.rebase(newItem) + return True + + def Undo(self): + cmd = FitRebaseItemCommand(self.fitID, self.containerName, self.position, self.oldTypeID) + return cmd.Do() diff --git a/gui/fitCommands/calc/fitReplaceModule.py b/gui/fitCommands/calc/fitReplaceModule.py index 286bd9451..1a171ea3b 100644 --- a/gui/fitCommands/calc/fitReplaceModule.py +++ b/gui/fitCommands/calc/fitReplaceModule.py @@ -2,7 +2,8 @@ import wx from logbook import Logger import eos.db -from eos.saveddata.module import Module, State +from eos.saveddata.module import Module +from eos.const import FittingModuleState from gui.fitCommands.helpers import ModuleInfoCache pyfalog = Logger(__name__) @@ -72,19 +73,21 @@ class FitReplaceModuleCommand(wx.Command): # Dummy it out in case the next bit fails fit.modules.toDummy(self.position) - if self.module.fits(fit): - self.module.owner = fit - fit.modules.toModule(self.position, self.module) - if self.module.isValidState(State.ACTIVE): - self.module.state = State.ACTIVE + if not self.module.fits(fit): + self.Undo() + return False - if self.old_module and self.old_module.charge and self.module.isValidCharge(self.old_module.charge): - self.module.charge = self.old_module.charge + self.module.owner = fit + fit.modules.toModule(self.position, self.module) + if self.module.isValidState(FittingModuleState.ACTIVE): + self.module.state = FittingModuleState.ACTIVE - # Then, check states of all modules and change where needed. This will recalc if needed - # self.checkStates(fit, m) + if self.old_module and self.old_module.charge and self.module.isValidCharge(self.old_module.charge): + self.module.charge = self.old_module.charge - # fit.fill() - eos.db.commit() - return True - return False + # Then, check states of all modules and change where needed. This will recalc if needed + # self.checkStates(fit, m) + + # fit.fill() + eos.db.commit() + return True diff --git a/gui/fitCommands/calc/fitSetSpoolup.py b/gui/fitCommands/calc/fitSetSpoolup.py new file mode 100644 index 000000000..9274d94bc --- /dev/null +++ b/gui/fitCommands/calc/fitSetSpoolup.py @@ -0,0 +1,37 @@ +import wx +import eos.db +from logbook import Logger +from eos.saveddata.booster import Booster +pyfalog = Logger(__name__) + + +class FitSetSpoolupCommand(wx.Command): + def __init__(self, fitID, position, spoolType, spoolAmount): + wx.Command.__init__(self, True) + self.fitID = fitID + self.position = position + self.spoolType = spoolType + self.spoolAmount = spoolAmount + self.projected = False # todo: get this to work with projected modules? Is that a thing? + self.cache = None + + def Do(self): + return self.__set(self.spoolType, self.spoolAmount) + + def Undo(self): + if self.cache: + self.__set(*self.cache) + return True + + def __set(self, type, amount): + fit = eos.db.getFit(self.fitID) + source = fit.modules if not self.projected else fit.projectedModules + + mod = source[self.position] + self.cache = mod.spoolType, mod.spoolAmount + + mod.spoolType = type + mod.spoolAmount = amount + + eos.db.commit() + return True diff --git a/gui/fitCommands/guiAddDrone.py b/gui/fitCommands/guiAddDrone.py index 6549d1e21..3c1bec21e 100644 --- a/gui/fitCommands/guiAddDrone.py +++ b/gui/fitCommands/guiAddDrone.py @@ -8,7 +8,7 @@ from .calc.fitAddDrone import FitAddDroneCommand class GuiAddDroneCommand(wx.Command): def __init__(self, fitID, itemID): - wx.Command.__init__(self, True, "Cargo Add") + wx.Command.__init__(self, True, "Drone Add") self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.internal_history = wx.CommandProcessor() diff --git a/gui/fitCommands/guiAddFighter.py b/gui/fitCommands/guiAddFighter.py index 01024e706..7a69ebc28 100644 --- a/gui/fitCommands/guiAddFighter.py +++ b/gui/fitCommands/guiAddFighter.py @@ -8,7 +8,7 @@ from .calc.fitAddFighter import FitAddFighterCommand class GuiAddFighterCommand(wx.Command): def __init__(self, fitID, itemID): - wx.Command.__init__(self, True, "Cargo Add") + wx.Command.__init__(self, True, "Fighter Add") self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.internal_history = wx.CommandProcessor() diff --git a/gui/fitCommands/guiAddImplant.py b/gui/fitCommands/guiAddImplant.py index 79b75f014..52b284071 100644 --- a/gui/fitCommands/guiAddImplant.py +++ b/gui/fitCommands/guiAddImplant.py @@ -3,7 +3,7 @@ from service.fit import Fit import gui.mainFrame from gui import globalEvents as GE -from eos.saveddata.fit import ImplantLocation +from eos.const import ImplantLocation from .calc.fitAddImplant import FitAddImplantCommand from .calc.fitChangeImplantLocation import FitChangeImplantLocation diff --git a/gui/fitCommands/guiCargoToModule.py b/gui/fitCommands/guiCargoToModule.py index a2583e47c..d77abbc3b 100644 --- a/gui/fitCommands/guiCargoToModule.py +++ b/gui/fitCommands/guiCargoToModule.py @@ -21,7 +21,7 @@ class GuiCargoToModuleCommand(wx.Command): """ def __init__(self, fitID, moduleIdx, cargoIdx, copy=False): - wx.Command.__init__(self, True, "Module State Change") + wx.Command.__init__(self, True, "Cargo to Module") self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.fitID = fitID diff --git a/gui/fitCommands/guiFillWithModule.py b/gui/fitCommands/guiFillWithModule.py index 0fa962bef..5b7bf95e3 100644 --- a/gui/fitCommands/guiFillWithModule.py +++ b/gui/fitCommands/guiFillWithModule.py @@ -17,7 +17,7 @@ class GuiFillWithModuleCommand(wx.Command): set the charge on the underlying module (requires position) :param position: Optional. The position in fit.modules that we are attempting to set the item to """ - wx.Command.__init__(self, True, "Module Add: {}".format(itemID)) + wx.Command.__init__(self, True, "Module Fill: {}".format(itemID)) self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.fitID = fitID diff --git a/gui/fitCommands/guiModuleToCargo.py b/gui/fitCommands/guiModuleToCargo.py index 66a9f9833..4f5b6bcf8 100644 --- a/gui/fitCommands/guiModuleToCargo.py +++ b/gui/fitCommands/guiModuleToCargo.py @@ -14,7 +14,7 @@ pyfalog = Logger(__name__) class GuiModuleToCargoCommand(wx.Command): def __init__(self, fitID, moduleIdx, cargoIdx, copy=False): - wx.Command.__init__(self, True, "Module State Change") + wx.Command.__init__(self, True, "Module to Cargo") self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.fitID = fitID diff --git a/gui/fitCommands/guiRebaseItems.py b/gui/fitCommands/guiRebaseItems.py new file mode 100644 index 000000000..8a69a78f5 --- /dev/null +++ b/gui/fitCommands/guiRebaseItems.py @@ -0,0 +1,46 @@ +import wx + +import eos.db +import gui.mainFrame +from gui import globalEvents as GE +from service.fit import Fit +from .calc.fitRebaseItem import FitRebaseItemCommand +from .calc.fitSetCharge import FitSetChargeCommand + + +class GuiRebaseItemsCommand(wx.Command): + + def __init__(self, fitID, rebaseMap): + wx.Command.__init__(self, True, "Mass Rebase Item") + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.fitID = fitID + self.rebaseMap = rebaseMap + self.internal_history = wx.CommandProcessor() + + def Do(self): + fit = eos.db.getFit(self.fitID) + for mod in fit.modules: + if mod.item is not None and mod.item.ID in self.rebaseMap: + self.internal_history.Submit(FitRebaseItemCommand(self.fitID, "modules", mod.modPosition, self.rebaseMap[mod.item.ID])) + if mod.charge is not None and mod.charge.ID in self.rebaseMap: + self.internal_history.Submit(FitSetChargeCommand(self.fitID, [mod.modPosition], self.rebaseMap[mod.charge.ID])) + for containerName in ("drones", "fighters", "implants", "boosters", "cargo"): + container = getattr(fit, containerName) + for obj in container: + if obj.item is not None and obj.item.ID in self.rebaseMap: + self.internal_history.Submit(FitRebaseItemCommand(self.fitID, containerName, container.index(obj), self.rebaseMap[obj.item.ID])) + if self.internal_history.Commands: + eos.db.commit() + Fit.getInstance().recalc(self.fitID) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID)) + return True + else: + return False + + def Undo(self): + for _ in self.internal_history.Commands: + self.internal_history.Undo() + eos.db.commit() + Fit.getInstance().recalc(self.fitID) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID)) + return True diff --git a/gui/fitCommands/guiRemoveCargo.py b/gui/fitCommands/guiRemoveCargo.py index f9aa5872a..54323a30e 100644 --- a/gui/fitCommands/guiRemoveCargo.py +++ b/gui/fitCommands/guiRemoveCargo.py @@ -8,7 +8,7 @@ from .calc.fitRemoveCargo import FitRemoveCargoCommand class GuiRemoveCargoCommand(wx.Command): def __init__(self, fitID, itemID): - wx.Command.__init__(self, True, "Module Charge Add") + wx.Command.__init__(self, True, "Cargo Remove") self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.internal_history = wx.CommandProcessor() diff --git a/gui/fitCommands/guiRemoveDrone.py b/gui/fitCommands/guiRemoveDrone.py index 42e651e3d..0d3f9770d 100644 --- a/gui/fitCommands/guiRemoveDrone.py +++ b/gui/fitCommands/guiRemoveDrone.py @@ -8,7 +8,7 @@ from .calc.fitRemoveDrone import FitRemoveDroneCommand class GuiRemoveDroneCommand(wx.Command): def __init__(self, fitID, position, amount=1): - wx.Command.__init__(self, True, "Cargo Add") + wx.Command.__init__(self, True, "Drone Remove") self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.internal_history = wx.CommandProcessor() diff --git a/gui/fitCommands/guiRemoveFighter.py b/gui/fitCommands/guiRemoveFighter.py index f1b983ec5..c1f283700 100644 --- a/gui/fitCommands/guiRemoveFighter.py +++ b/gui/fitCommands/guiRemoveFighter.py @@ -8,7 +8,7 @@ from .calc.fitRemoveFighter import FitRemoveFighterCommand class GuiRemoveFighterCommand(wx.Command): def __init__(self, fitID, position): - wx.Command.__init__(self, True, "Module Remove") + wx.Command.__init__(self, True, "Fighter Remove") self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.fitID = fitID diff --git a/gui/fitCommands/guiRemoveProjected.py b/gui/fitCommands/guiRemoveProjected.py index 74d1ab308..7fb54e5fc 100644 --- a/gui/fitCommands/guiRemoveProjected.py +++ b/gui/fitCommands/guiRemoveProjected.py @@ -27,7 +27,7 @@ class GuiRemoveProjectedCommand(wx.Command): } def __init__(self, fitID, thing): - wx.Command.__init__(self, True, "Projected Add") + wx.Command.__init__(self, True, "Projected Remove") self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.internal_history = wx.CommandProcessor() diff --git a/gui/fitCommands/guiSetMode.py b/gui/fitCommands/guiSetMode.py index 9639028f9..f7e5e09be 100644 --- a/gui/fitCommands/guiSetMode.py +++ b/gui/fitCommands/guiSetMode.py @@ -8,7 +8,7 @@ from .calc.fitSetMode import FitSetModeCommand class GuiSetModeCommand(wx.Command): def __init__(self, fitID, mode): - wx.Command.__init__(self, True, "Cargo Add") + wx.Command.__init__(self, True, "Mode Set") self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.sFit = Fit.getInstance() self.internal_history = wx.CommandProcessor() diff --git a/gui/fitCommands/guiSetSpoolup.py b/gui/fitCommands/guiSetSpoolup.py new file mode 100644 index 000000000..60f0c6f1e --- /dev/null +++ b/gui/fitCommands/guiSetSpoolup.py @@ -0,0 +1,32 @@ +import wx +from service.fit import Fit + +import gui.mainFrame +from gui import globalEvents as GE +from .calc.fitSetSpoolup import FitSetSpoolupCommand + + +class GuiSetSpoolup(wx.Command): + def __init__(self, fitID, module, spoolupType, spoolupAmount): + wx.Command.__init__(self, True, "Booster Add") + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.sFit = Fit.getInstance() + self.internal_history = wx.CommandProcessor() + self.fitID = fitID + self.position = module.modPosition + self.spoolType = spoolupType + self.spoolupAmount = spoolupAmount + + def Do(self): + if self.internal_history.Submit(FitSetSpoolupCommand(self.fitID, self.position, self.spoolType, self.spoolupAmount)): + self.sFit.recalc(self.fitID) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID)) + return True + return False + + def Undo(self): + for _ in self.internal_history.Commands: + self.internal_history.Undo() + self.sFit.recalc(self.fitID) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID)) + return True diff --git a/gui/mainFrame.py b/gui/mainFrame.py index f7039b73e..3d17f9f65 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -37,7 +37,6 @@ import config import gui.globalEvents as GE from eos.config import gamedata_date, gamedata_version from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues -from eos.db.saveddata.queries import getFit as db_getFit # import this to access override setting from eos.modifiedAttributeDict import ModifiedAttributeDict from gui import graphFrame @@ -64,11 +63,12 @@ from gui.setEditor import ImplantSetEditorDlg from gui.shipBrowser import ShipBrowser from gui.statsPane import StatsPane from gui.updateDialog import UpdateDialog -from gui.utils.clipboard import fromClipboard, toClipboard +from gui.utils.clipboard import fromClipboard from service.character import Character from service.esi import Esi from service.fit import Fit -from service.port import EfsPort, IPortUser, Port +from service.port import IPortUser, Port +from service.price import Price from service.settings import HTMLExportSettings, SettingsProvider from service.update import Update import gui.fitCommands as cmd @@ -508,6 +508,8 @@ class MainFrame(wx.Frame): self.Bind(wx.EVT_MENU, self.saveCharAs, id=menuBar.saveCharAsId) # Save current character self.Bind(wx.EVT_MENU, self.revertChar, id=menuBar.revertCharId) + # Optimize fit price + self.Bind(wx.EVT_MENU, self.optimizeFitPrice, id=menuBar.optimizeFitPrice) # Browse fittings self.Bind(wx.EVT_MENU, self.eveFittings, id=menuBar.eveFittingsId) @@ -655,6 +657,23 @@ class MainFrame(wx.Frame): sChr.revertCharacter(charID) wx.PostEvent(self, GE.CharListUpdated()) + def optimizeFitPrice(self, event): + fitID = self.getActiveFit() + sFit = Fit.getInstance() + fit = sFit.getFit(fitID) + + if fit: + def updateFitCb(replacementsCheaper): + del self.waitDialog + del self.disablerAll + rebaseMap = {k.ID: v.ID for k, v in replacementsCheaper.items()} + self.command.Submit(cmd.GuiRebaseItemsCommand(fitID, rebaseMap)) + + fitItems = {i for i in Fit.fitItemIter(fit) if i is not fit.ship.item} + self.disablerAll = wx.WindowDisabler() + self.waitDialog = wx.BusyInfo("Please Wait...", parent=self) + Price.getInstance().findCheaperReplacements(fitItems, updateFitCb, fetchTimeout=10) + def AdditionsTabSelect(self, event): selTab = self.additionsSelect.index(event.GetId()) @@ -688,30 +707,6 @@ class MainFrame(wx.Frame): else: self.marketBrowser.search.Focus() - def clipboardEft(self, options): - fit = db_getFit(self.getActiveFit()) - toClipboard(Port.exportEft(fit, options)) - - def clipboardDna(self, options): - fit = db_getFit(self.getActiveFit()) - toClipboard(Port.exportDna(fit)) - - def clipboardEsi(self, options): - fit = db_getFit(self.getActiveFit()) - toClipboard(Port.exportESI(fit)) - - def clipboardXml(self, options): - fit = db_getFit(self.getActiveFit()) - toClipboard(Port.exportXml(None, fit)) - - def clipboardMultiBuy(self, options): - fit = db_getFit(self.getActiveFit()) - toClipboard(Port.exportMultiBuy(fit, options)) - - def clipboardEfs(self, options): - fit = db_getFit(self.getActiveFit()) - toClipboard(EfsPort.exportEfs(fit, 0)) - def importFromClipboard(self, event): clipboard = fromClipboard() activeFit = self.getActiveFit() @@ -728,28 +723,8 @@ class MainFrame(wx.Frame): self._openAfterImport(importData) def exportToClipboard(self, event): - CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft, - CopySelectDialog.copyFormatXml: self.clipboardXml, - CopySelectDialog.copyFormatDna: self.clipboardDna, - CopySelectDialog.copyFormatEsi: self.clipboardEsi, - CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy, - CopySelectDialog.copyFormatEfs: self.clipboardEfs} - dlg = CopySelectDialog(self) - btnPressed = dlg.ShowModal() - - if btnPressed == wx.ID_OK: - selected = dlg.GetSelected() - options = dlg.GetOptions() - - settings = SettingsProvider.getInstance().getSettings("pyfaExport") - settings["format"] = selected - settings["options"] = options - CopySelectDict[selected](options.get(selected)) - - try: - dlg.Destroy() - except RuntimeError: - pyfalog.error("Tried to destroy an object that doesn't exist in .") + with CopySelectDialog(self) as dlg: + dlg.ShowModal() def exportSkillsNeeded(self, event): """ Exports skills needed for active fit and active character """ diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index c0a28f1dc..413098b61 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -28,8 +28,6 @@ import gui.globalEvents as GE from gui.bitmap_loader import BitmapLoader from logbook import Logger -# from service.crest import Crest -# from service.crest import CrestModes pyfalog = Logger(__name__) @@ -59,6 +57,7 @@ class MainMenuBar(wx.MenuBar): self.importDatabaseDefaultsId = wx.NewId() self.toggleIgnoreRestrictionID = wx.NewId() self.devToolsId = wx.NewId() + self.optimizeFitPrice = wx.NewId() # pheonix: evaluate if this is needed if 'wxMac' in wx.PlatformInfo and wx.VERSION >= (3, 0): @@ -101,6 +100,9 @@ class MainMenuBar(wx.MenuBar): editMenu.Append(self.revertCharId, "Revert Character") editMenu.AppendSeparator() self.ignoreRestrictionItem = editMenu.Append(self.toggleIgnoreRestrictionID, "Ignore Fitting Restrictions") + editMenu.AppendSeparator() + editMenu.Append(self.optimizeFitPrice, "Optimize Fit Price") + # Character menu windowMenu = wx.Menu() @@ -134,8 +136,6 @@ class MainMenuBar(wx.MenuBar): preferencesItem.SetBitmap(BitmapLoader.getBitmap("preferences_small", "gui")) windowMenu.Append(preferencesItem) - # self.sEsi = Crest.getInstance() - # CREST Menu esiMMenu = wx.Menu() self.Append(esiMMenu, "EVE &SSO") diff --git a/gui/shipBrowser.py b/gui/shipBrowser.py index 75152a1b4..ed490194e 100644 --- a/gui/shipBrowser.py +++ b/gui/shipBrowser.py @@ -164,12 +164,12 @@ class ShipBrowser(wx.Panel): self.categoryList = list(sMkt.getShipRoot()) self.categoryList.sort(key=lambda _ship: _ship.name) + counts = sFit.countAllFitsGroupedByShip() + # set map & cache of fittings per category for cat in self.categoryList: - itemIDs = [x.ID for x in cat.items] - num = sFit.countFitsWithShip(itemIDs) - self.categoryFitCache[cat.ID] = num > 0 - + itemIDs = [x.ID for x in sMkt.getItemsByGroup(cat)] + self.categoryFitCache[cat.ID] = sum([count for shipID, count in counts if shipID in itemIDs]) > 0 for ship in self.categoryList: if self.filterShipsWithNoFits and not self.categoryFitCache[ship.ID]: continue diff --git a/gui/utils/exportHtml.py b/gui/utils/exportHtml.py index d1c0af9d0..608b52396 100644 --- a/gui/utils/exportHtml.py +++ b/gui/utils/exportHtml.py @@ -2,6 +2,7 @@ import threading import time # noinspection PyPackageRequirements import wx +from service.const import PortEftOptions from service.settings import HTMLExportSettings from service.fit import Fit from service.port import Port @@ -208,8 +209,10 @@ class exportHtmlThread(threading.Thread): if self.stopRunning: return try: - eftFit = Port.exportEft(getFit(fit[0])) - print(eftFit) + eftFit = Port.exportEft(getFit(fit[0]), options={ + PortEftOptions.IMPLANTS: True, + PortEftOptions.MUTATIONS: True, + PortEftOptions.LOADED_CHARGES: True}) HTMLfit = ( '
  • = attrs2[aid] and attrHig[aid]) or - (attrs1[aid] <= attrs2[aid] and not attrHig[aid]) - for aid in attrs1 - ): - return 2 - if all( - (attrs2[aid] >= attrs1[aid] and attrHig[aid]) or - (attrs2[aid] <= attrs1[aid] and not attrHig[aid]) - for aid in attrs1 - ): - return 3 - return 0 + return True + return False + print('finding replacements') skillReqAttribs = { 182: 277, 183: 278, @@ -213,10 +198,17 @@ def main(db, json_path): 1290: 1288} skillReqAttribsFlat = set(skillReqAttribs.keys()).union(skillReqAttribs.values()) # Get data on type groups + # Format: {type ID: group ID} typesGroups = {} for row in tables['evetypes']: typesGroups[row['typeID']] = row['groupID'] + # Get data on item effects + # Format: {type ID: set(effect, IDs)} + typesEffects = {} + for row in tables['dgmtypeeffects']: + typesEffects.setdefault(row['typeID'], set()).add(row['effectID']) # Get data on type attributes + # Format: {type ID: {attribute ID: attribute value}} typesNormalAttribs = {} typesSkillAttribs = {} for row in tables['dgmtypeattribs']: @@ -226,15 +218,23 @@ def main(db, json_path): typeSkillAttribs[row['attributeID']] = row['value'] # Ignore these attributes for comparison purposes elif attributeID in ( + # We do not need mass as it affects final ship stats only when carried by ship itself + # (and we're not going to replace ships), but it's wildly inconsistent for other items, + # which otherwise would be the same + 4, # mass + 124, # mainColor + 162, # radius 422, # techLevel 633, # metaLevel - 1692 # metaGroupID + 1692, # metaGroupID + 1768 # typeColorScheme ): continue else: typeNormalAttribs = typesNormalAttribs.setdefault(row['typeID'], {}) typeNormalAttribs[row['attributeID']] = row['value'] # Get data on skill requirements + # Format: {type ID: {skill type ID: skill level}} typesSkillReqs = {} for typeID, typeAttribs in typesSkillAttribs.items(): typeSkillAttribs = typesSkillAttribs.get(typeID, {}) @@ -248,46 +248,54 @@ def main(db, json_path): except (KeyError, ValueError): continue typeSkillReqs[skillType] = skillLevel - # Get data on attribute highIsGood flag - attrHig = {} - for row in tables['dgmattribs']: - attrHig[row['attributeID']] = bool(row['highIsGood']) + # Format: {group ID: category ID} + groupCategories = {} + for row in tables['evegroups']: + groupCategories[row['groupID']] = row['categoryID'] # As EVE affects various types mostly depending on their group or skill requirements, # we're going to group various types up this way + # Format: {(group ID, frozenset(skillreq, type, IDs), frozenset(type, effect, IDs): [type ID, {attribute ID: attribute value}]} groupedData = {} for row in tables['evetypes']: typeID = row['typeID'] + # Ignore items outside of categories we need + if groupCategories[typesGroups[typeID]] not in ( + 6, # Ship + 7, # Module + 8, # Charge + 18, # Drone + 20, # Implant + 22, # Deployable + 23, # Starbase + 32, # Subsystem + 35, # Decryptors + 65, # Structure + 66, # Structure Module + 87, # Fighter + ): + continue typeAttribs = typesNormalAttribs.get(typeID, {}) - # Ignore stuff w/o attributes + # Ignore items w/o attributes if not typeAttribs: continue # We need only skill types, not levels for keys typeSkillreqs = frozenset(typesSkillReqs.get(typeID, {})) typeGroup = typesGroups[typeID] - groupData = groupedData.setdefault((typeGroup, typeSkillreqs), []) + typeEffects = frozenset(typesEffects.get(typeID, ())) + groupData = groupedData.setdefault((typeGroup, typeSkillreqs, typeEffects), []) groupData.append((typeID, typeAttribs)) - same = {} - better = {} - # Now, go through composed groups and for every item within it find items which are - # the same and which are better + # Format: {type ID: set(type IDs)} + replacements = {} + # Now, go through composed groups and for every item within it + # find items which are the same for groupData in groupedData.values(): for type1, type2 in itertools.combinations(groupData, 2): - comparisonResult = compareAttrs(type1[1], type2[1], attrHig) - # Equal - if comparisonResult == 1: - same.setdefault(type1[0], set()).add(type2[0]) - same.setdefault(type2[0], set()).add(type1[0]) - # First is better - elif comparisonResult == 2: - better.setdefault(type2[0], set()).add(type1[0]) - # Second is better - elif comparisonResult == 3: - better.setdefault(type1[0], set()).add(type2[0]) + if compareAttrs(type1[1], type2[1]): + replacements.setdefault(type1[0], set()).add(type2[0]) + replacements.setdefault(type2[0], set()).add(type1[0]) # Put this data into types table so that normal process hooks it up for row in tables['evetypes']: - typeID = row['typeID'] - row['replaceSame'] = ','.join('{}'.format(tid) for tid in sorted(same.get(typeID, ()))) - row['replaceBetter'] = ','.join('{}'.format(tid) for tid in sorted(better.get(typeID, ()))) + row['replacements'] = ','.join('{}'.format(tid) for tid in sorted(replacements.get(row['typeID'], ()))) data = {} diff --git a/service/character.py b/service/character.py index 565d7d329..7e95c5f82 100644 --- a/service/character.py +++ b/service/character.py @@ -37,7 +37,8 @@ from service.esi import Esi from eos.saveddata.implant import Implant as es_Implant from eos.saveddata.character import Character as es_Character, Skill -from eos.saveddata.module import Slot as es_Slot, Module as es_Module +from eos.saveddata.module import Module as es_Module +from eos.const import FittingSlot as es_Slot from eos.saveddata.fighter import Fighter as es_Fighter pyfalog = Logger(__name__) diff --git a/service/const.py b/service/const.py new file mode 100644 index 000000000..d09d809f6 --- /dev/null +++ b/service/const.py @@ -0,0 +1,104 @@ +# ============================================================================= +# Copyright (C) 2019 Ryan Holmes +# +# This file is part of pyfa. +# +# pyfa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pyfa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyfa. If not, see . +# ============================================================================= + +from enum import Enum, IntEnum, unique, auto + + +@unique +class EsiLoginMethod(IntEnum): + """ + Contains the method of ESI login + """ + SERVER = 0 + MANUAL = 1 + + +@unique +class EsiSsoMode(IntEnum): + """ + Contains the mode of ESI sso mode + """ + AUTO = 0 + CUSTOM = 1 + + +class EsiEndpoints(Enum): + """ + Contains the endpoint paths for the ESI access + """ + CHAR = "/v4/characters/{character_id}/" + CHAR_SKILLS = "/v4/characters/{character_id}/skills/" + CHAR_FITTINGS = "/v1/characters/{character_id}/fittings/" + CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/" + + +@unique +class PortMultiBuyOptions(IntEnum): + """ + Contains different types of items for multibuy export + """ + IMPLANTS = 1 + CARGO = 2 + LOADED_CHARGES = 3 + OPTIMIZE_PRICES = 4 + + +@unique +class PortEftOptions(IntEnum): + """ + Contains different options for eft-export + """ + IMPLANTS = 1 + MUTATIONS = 2 + LOADED_CHARGES = 3 + + +@unique +class PortEftRigSize(IntEnum): + """ + Contains different sizes of ship rigs + This enum is not actively used, but maybe useful someday. + """ + SMALL = 1 + MEDIUM = 2 + LARGE = 3 + CAPITAL = 4 + + +@unique +class GuiAttrGroup(IntEnum): + """ + Define the various groups of attributes. + This enum is used for GUI functions and getting redefined in + /gui/builtinItemStatsViews/attributeGrouping.py + """ + FITTING = auto() + STRUCTURE = auto() + SHIELD = auto() + ARMOR = auto() + TARGETING = auto() + EWAR_RESISTS = auto() + CAPACITOR = auto() + SHARED_FACILITIES = auto() + FIGHTER_FACILITIES = auto() + ON_DEATH = auto() + JUMP_SYSTEMS = auto() + PROPULSIONS = auto() + FIGHTERS = auto() + SHIP_GROUP = auto() \ No newline at end of file diff --git a/service/esi.py b/service/esi.py index 8162c6844..88b704664 100644 --- a/service/esi.py +++ b/service/esi.py @@ -9,9 +9,9 @@ import config import webbrowser import eos.db -from eos.enum import Enum +from service.const import EsiLoginMethod, EsiSsoMode from eos.saveddata.ssocharacter import SsoCharacter -from service.esiAccess import APIException, SsoMode +from service.esiAccess import APIException import gui.globalEvents as GE from gui.ssoLogin import SsoLogin, SsoLoginServer from service.server import StoppableHTTPServer, AuthHandler @@ -24,11 +24,6 @@ from requests import Session pyfalog = Logger(__name__) -class LoginMethod(Enum): - SERVER = 0 - MANUAL = 1 - - class Esi(EsiAccess): _instance = None @@ -107,8 +102,8 @@ class Esi(EsiAccess): def login(self): # always start the local server if user is using client details. Otherwise, start only if they choose to do so. - if self.settings.get('ssoMode') == SsoMode.CUSTOM or self.settings.get('loginMode') == LoginMethod.SERVER: - dlg = gui.ssoLogin.SsoLoginServer(6461 if self.settings.get('ssoMode') == SsoMode.CUSTOM else 0) + if self.settings.get('ssoMode') == EsiSsoMode.CUSTOM or self.settings.get('loginMode') == EsiLoginMethod.SERVER: + dlg = gui.ssoLogin.SsoLoginServer(6461 if self.settings.get('ssoMode') == EsiSsoMode.CUSTOM else 0) dlg.ShowModal() else: dlg = gui.ssoLogin.SsoLogin() @@ -142,7 +137,7 @@ class Esi(EsiAccess): def handleLogin(self, message): # we already have authenticated stuff for the auto mode - if self.settings.get('ssoMode') == SsoMode.AUTO: + if self.settings.get('ssoMode') == EsiSsoMode.AUTO: ssoInfo = message['SSOInfo'][0] auth_response = json.loads(base64.b64decode(ssoInfo)) else: diff --git a/service/esiAccess.py b/service/esiAccess.py index 8ec3dd95d..d89fc9b0b 100644 --- a/service/esiAccess.py +++ b/service/esiAccess.py @@ -17,7 +17,7 @@ import config import base64 import datetime -from eos.enum import Enum +from service.const import EsiSsoMode, EsiEndpoints from service.settings import EsiSettings, NetworkSettings from requests import Session @@ -42,11 +42,6 @@ scopes = [ ] -class SsoMode(Enum): - AUTO = 0 - CUSTOM = 1 - - class APIException(Exception): """ Exception for SSO related errors """ @@ -66,13 +61,6 @@ class APIException(Exception): return 'HTTP Error %s' % self.status_code -class ESIEndpoints(Enum): - CHAR = "/v4/characters/{character_id}/" - CHAR_SKILLS = "/v4/characters/{character_id}/skills/" - CHAR_FITTINGS = "/v1/characters/{character_id}/fittings/" - CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/" - - class EsiAccess(object): def __init__(self): self.settings = EsiSettings.getInstance() @@ -89,7 +77,7 @@ class EsiAccess(object): @property def sso_url(self): - if self.settings.get("ssoMode") == SsoMode.CUSTOM: + if self.settings.get("ssoMode") == EsiSsoMode.CUSTOM: return "https://login.eveonline.com" return "https://www.pyfa.io" @@ -110,20 +98,20 @@ class EsiAccess(object): return '%s/oauth/token' % self.sso_url def getSkills(self, char): - return self.get(char, ESIEndpoints.CHAR_SKILLS, character_id=char.characterID) + return self.get(char, EsiEndpoints.CHAR_SKILLS.value, character_id=char.characterID) def getSecStatus(self, char): - return self.get(char, ESIEndpoints.CHAR, character_id=char.characterID) + return self.get(char, EsiEndpoints.CHAR.value, character_id=char.characterID) def getFittings(self, char): - return self.get(char, ESIEndpoints.CHAR_FITTINGS, character_id=char.characterID) + return self.get(char, EsiEndpoints.CHAR_FITTINGS.value, character_id=char.characterID) def postFitting(self, char, json_str): # @todo: new fitting ID can be recovered from resp.data, - return self.post(char, ESIEndpoints.CHAR_FITTINGS, json_str, character_id=char.characterID) + return self.post(char, EsiEndpoints.CHAR_FITTINGS.value, json_str, character_id=char.characterID) def delFitting(self, char, fittingID): - return self.delete(char, ESIEndpoints.CHAR_DEL_FIT, character_id=char.characterID, fitting_id=fittingID) + return self.delete(char, EsiEndpoints.CHAR_DEL_FIT.value, character_id=char.characterID, fitting_id=fittingID) @staticmethod def update_token(char, tokenResponse): @@ -136,7 +124,7 @@ class EsiAccess(object): def getLoginURI(self, redirect=None): self.state = str(uuid.uuid4()) - if self.settings.get("ssoMode") == SsoMode.AUTO: + if self.settings.get("ssoMode") == EsiSsoMode.AUTO: args = { 'state': self.state, 'pyfa_version': config.version, @@ -183,7 +171,7 @@ class EsiAccess(object): 'refresh_token': refreshToken, } - if self.settings.get('ssoMode') == SsoMode.AUTO: + if self.settings.get('ssoMode') == EsiSsoMode.AUTO: # data is all we really need, the rest is handled automatically by pyfa.io return { 'data': data, diff --git a/service/fit.py b/service/fit.py index 035bb50ee..c02dd7159 100644 --- a/service/fit.py +++ b/service/fit.py @@ -30,8 +30,9 @@ from eos.saveddata.citadel import Citadel as es_Citadel from eos.saveddata.damagePattern import DamagePattern as es_DamagePattern from eos.saveddata.drone import Drone as es_Drone from eos.saveddata.fighter import Fighter as es_Fighter -from eos.saveddata.fit import Fit as FitType, ImplantLocation -from eos.saveddata.module import Module as es_Module, State +from eos.const import ImplantLocation, FittingModuleState +from eos.saveddata.fit import Fit as FitType +from eos.saveddata.module import Module as es_Module from eos.saveddata.ship import Ship as es_Ship from service.character import Character from service.damagePattern import DamagePattern @@ -146,6 +147,11 @@ class Fit(FitDeprecated): pyfalog.debug("Getting count of all fits.") return eos.db.countAllFits() + @staticmethod + def countAllFitsGroupedByShip(): + count = eos.db.countFitGroupedByShip() + return count + @staticmethod def countFitsWithShip(stuff): pyfalog.debug("Getting count of all fits for: {0}", stuff) @@ -347,7 +353,7 @@ class Fit(FitDeprecated): elif isinstance(thing, es_Module): thing.state = es_Module.getProposedState(thing, click) if not thing.canHaveState(thing.state, fit): - thing.state = State.OFFLINE + thing.state = FittingModuleState.OFFLINE elif isinstance(thing, FitType): projectionInfo = thing.getProjectionInfo(fitID) if projectionInfo: @@ -379,8 +385,8 @@ class Fit(FitDeprecated): if m.fits(fit): m.owner = fit fit.modules.toModule(position, m) - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE + if m.isValidState(FittingModuleState.ACTIVE): + m.state = FittingModuleState.ACTIVE # As some items may affect state-limiting attributes of the ship, calculate new attributes first self.recalc(fit) @@ -534,13 +540,13 @@ class Fit(FitDeprecated): if mod != base: # fix for #529, where a module may be in incorrect state after CCP changes mechanics of module if not mod.canHaveState(mod.state) or not mod.isValidState(mod.state): - mod.state = State.ONLINE + mod.state = FittingModuleState.ONLINE changed = True for mod in fit.projectedModules: # fix for #529, where a module may be in incorrect state after CCP changes mechanics of module if not mod.canHaveState(mod.state, fit) or not mod.isValidState(mod.state): - mod.state = State.OFFLINE + mod.state = FittingModuleState.OFFLINE changed = True for drone in fit.projectedDrones: @@ -549,9 +555,26 @@ class Fit(FitDeprecated): changed = True return changed - # If any state was changed, recalculate attributes again - # if changed: - # self.recalc(fit) + + @classmethod + def fitObjectIter(cls, fit): + yield fit.ship + + for mod in fit.modules: + if not mod.isEmpty: + yield mod + + for container in (fit.drones, fit.fighters, fit.implants, fit.boosters, fit.cargo): + for obj in container: + yield obj + + @classmethod + def fitItemIter(cls, fit): + for fitobj in cls.fitObjectIter(fit): + yield fitobj.item + charge = getattr(fitobj, 'charge', None) + if charge: + yield charge def refreshFit(self, fitID): pyfalog.debug("Refresh fit for fit ID: {0}", fitID) diff --git a/service/fitDeprecated.py b/service/fitDeprecated.py index 4f0d79b0d..138da90e6 100644 --- a/service/fitDeprecated.py +++ b/service/fitDeprecated.py @@ -27,7 +27,8 @@ from eos.saveddata.cargo import Cargo as es_Cargo from eos.saveddata.drone import Drone as es_Drone from eos.saveddata.fighter import Fighter as es_Fighter from eos.saveddata.implant import Implant as es_Implant -from eos.saveddata.module import Module as es_Module, State +from eos.saveddata.module import Module as es_Module +from eos.const import FittingModuleState from eos.saveddata.fit import Fit as FitType from utils.deprecated import deprecated @@ -304,16 +305,16 @@ class FitDeprecated(object): fit.projectedFighters.append(fighter) elif thing.group.name in es_Module.SYSTEM_GROUPS: module = es_Module(thing) - module.state = State.ONLINE + module.state = FittingModuleState.ONLINE fit.projectedModules.append(module) else: try: module = es_Module(thing) except ValueError: return False - module.state = State.ACTIVE + module.state = FittingModuleState.ACTIVE if not module.canHaveState(module.state, fit): - module.state = State.OFFLINE + module.state = FittingModuleState.OFFLINE fit.projectedModules.append(module) eos.db.commit() @@ -396,8 +397,8 @@ class FitDeprecated(object): m.owner = fit numSlots = len(fit.modules) fit.modules.append(m) - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE + if m.isValidState(FittingModuleState.ACTIVE): + m.state = FittingModuleState.ACTIVE # As some items may affect state-limiting attributes of the ship, calculate new attributes first self.recalc(fit) @@ -465,8 +466,8 @@ class FitDeprecated(object): if m.fits(fit): m.owner = fit fit.modules.toModule(position, m) - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE + if m.isValidState(FittingModuleState.ACTIVE): + m.state = FittingModuleState.ACTIVE if recalc: # As some items may affect state-limiting attributes of the ship, calculate new attributes first @@ -508,8 +509,8 @@ class FitDeprecated(object): try: cargoP = es_Module(cargo.item) cargoP.owner = fit - if cargoP.isValidState(State.ACTIVE): - cargoP.state = State.ACTIVE + if cargoP.isValidState(FittingModuleState.ACTIVE): + cargoP.state = FittingModuleState.ACTIVE except: pyfalog.warning("Invalid item: {0}", cargo.item) return diff --git a/service/market.py b/service/market.py index 745909e23..085fef624 100644 --- a/service/market.py +++ b/service/market.py @@ -795,3 +795,20 @@ class Market(object): """Filter items by meta lvl""" filtered = set([item for item in items if self.getMetaGroupIdByItem(item) in metas]) return filtered + + def getReplacements(self, identity): + item = self.getItem(identity) + # We already store needed type IDs in database + replTypeIDs = {int(i) for i in item.replacements.split(",") if i} + if not replTypeIDs: + return () + # As replacements were generated without keeping track which items were published, + # filter them out here + items = [] + for typeID in replTypeIDs: + item = self.getItem(typeID) + if not item: + continue + if self.getPublicityByItem(item): + items.append(item) + return items diff --git a/service/marketSources/evemarketdata.py b/service/marketSources/evemarketdata.py index 15edc92ef..b592c9067 100644 --- a/service/marketSources/evemarketdata.py +++ b/service/marketSources/evemarketdata.py @@ -17,30 +17,37 @@ # along with pyfa. If not, see . # ============================================================================= -import time + from xml.dom import minidom from logbook import Logger from eos.saveddata.price import PriceStatus from service.network import Network -from service.price import Price, TIMEOUT, VALIDITY +from service.price import Price pyfalog = Logger(__name__) -class EveMarketData(object): +class EveMarketData: name = "eve-marketdata.com" - def __init__(self, types, system, priceMap): - data = {} - baseurl = "https://eve-marketdata.com/api/item_prices.xml" - data["system_id"] = system # Use Jita for market - data["type_ids"] = ','.join(str(x) for x in types) + def __init__(self, priceMap, system, fetchTimeout): + # Try selected system first + self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system) + # If price was not available - try globally + if priceMap: + self.fetchPrices(priceMap, max(fetchTimeout / 3, 2)) + @staticmethod + def fetchPrices(priceMap, fetchTimeout, system=None): + params = {"type_ids": ','.join(str(typeID) for typeID in priceMap)} + if system is not None: + params["system_id"] = system + baseurl = "https://eve-marketdata.com/api/item_prices.xml" network = Network.getInstance() - data = network.request(baseurl, network.PRICES, params=data) + data = network.request(baseurl, network.PRICES, params=params, timeout=fetchTimeout) xml = minidom.parseString(data.text) types = xml.getElementsByTagName("eve").item(0).getElementsByTagName("price") @@ -55,19 +62,10 @@ class EveMarketData(object): pyfalog.warning("Failed to get price for: {0}", type_) continue - # Fill price data - priceobj = priceMap[typeID] - - # eve-marketdata returns 0 if price data doesn't even exist for the item. In this case, don't reset the - # cached price, and set the price timeout to TIMEOUT (every 15 minutes currently). Se GH issue #1334 - if price != 0: - priceobj.price = price - priceobj.time = time.time() + VALIDITY - priceobj.status = PriceStatus.success - else: - priceobj.time = time.time() + TIMEOUT - - # delete price from working dict + # eve-marketdata returns 0 if price data doesn't even exist for the item + if price == 0: + continue + priceMap[typeID].update(PriceStatus.fetchSuccess, price) del priceMap[typeID] diff --git a/service/marketSources/evemarketer.py b/service/marketSources/evemarketer.py index 478de4371..6d85380ab 100644 --- a/service/marketSources/evemarketer.py +++ b/service/marketSources/evemarketer.py @@ -17,33 +17,37 @@ # along with pyfa. If not, see . # ============================================================================= -import time + from xml.dom import minidom from logbook import Logger from eos.saveddata.price import PriceStatus from service.network import Network -from service.price import Price, VALIDITY +from service.price import Price pyfalog = Logger(__name__) -class EveMarketer(object): +class EveMarketer: name = "evemarketer" - def __init__(self, types, system, priceMap): - data = {} + def __init__(self, priceMap, system, fetchTimeout): + # Try selected system first + self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system) + # If price was not available - try globally + if priceMap: + self.fetchPrices(priceMap, max(fetchTimeout / 3, 2)) + + @staticmethod + def fetchPrices(priceMap, fetchTimeout, system=None): + params = {"typeid": {typeID for typeID in priceMap}} + if system is not None: + params["usesystem"] = system baseurl = "https://api.evemarketer.com/ec/marketstat" - - data["usesystem"] = system # Use Jita for market - data["typeid"] = set() - for typeID in types: # Add all typeID arguments - data["typeid"].add(typeID) - network = Network.getInstance() - data = network.request(baseurl, network.PRICES, params=data) + data = network.request(baseurl, network.PRICES, params=params, timeout=fetchTimeout) xml = minidom.parseString(data.text) types = xml.getElementsByTagName("marketstat").item(0).getElementsByTagName("type") # Cycle through all types we've got from request @@ -56,15 +60,15 @@ class EveMarketer(object): percprice = float(sell.getElementsByTagName("percentile").item(0).firstChild.data) except (TypeError, ValueError): pyfalog.warning("Failed to get price for: {0}", type_) - percprice = 0 + continue - # Fill price data - priceobj = priceMap[typeID] - priceobj.price = percprice - priceobj.time = time.time() + VALIDITY - priceobj.status = PriceStatus.success + # Price is 0 if evemarketer has info on this item, but it is not available + # for current scope limit. If we provided scope limit - make sure to skip + # such items to check globally, and do not skip if requested globally + if percprice == 0 and system is not None: + continue - # delete price from working dict + priceMap[typeID].update(PriceStatus.fetchSuccess, percprice) del priceMap[typeID] diff --git a/service/network.py b/service/network.py index 5554eadec..5ce413445 100644 --- a/service/network.py +++ b/service/network.py @@ -53,7 +53,7 @@ class TimeoutError(Exception): pass -class Network(object): +class Network: # Request constants - every request must supply this, as it is checked if # enabled or not via settings ENABLED = 1 diff --git a/service/port/dna.py b/service/port/dna.py index bc64e9e99..9b1a5ecee 100644 --- a/service/port/dna.py +++ b/service/port/dna.py @@ -28,8 +28,9 @@ 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.module import Module from eos.saveddata.ship import Ship +from eos.const import FittingSlot, FittingModuleState from service.fit import Fit as svcFit from service.market import Market @@ -106,8 +107,8 @@ def importDna(string): f.modules.append(m) else: m.owner = f - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE + if m.isValidState(FittingModuleState.ACTIVE): + m.state = FittingModuleState.ACTIVE moduleList.append(m) # Recalc to get slot numbers correct for T3 cruisers @@ -116,14 +117,14 @@ def importDna(string): for module in moduleList: if module.fits(f): module.owner = f - if module.isValidState(State.ACTIVE): - module.state = State.ACTIVE + if module.isValidState(FittingModuleState.ACTIVE): + module.state = FittingModuleState.ACTIVE f.modules.append(module) return f -def exportDna(fit): +def exportDna(fit, callback): dna = str(fit.shipID) subsystems = [] # EVE cares which order you put these in mods = OrderedDict() @@ -131,7 +132,7 @@ def exportDna(fit): sFit = svcFit.getInstance() for mod in fit.modules: if not mod.isEmpty: - if mod.slot == Slot.SUBSYSTEM: + if mod.slot == FittingSlot.SUBSYSTEM: subsystems.append(mod) continue if mod.itemID not in mods: @@ -173,4 +174,9 @@ def exportDna(fit): for charge in charges: dna += ":{0};{1}".format(charge, charges[charge]) - return dna + "::" + text = dna + "::" + + if callback: + callback(text) + else: + return text diff --git a/service/port/efs.py b/service/port/efs.py index c93c7d87d..fc00d5dfb 100755 --- a/service/port/efs.py +++ b/service/port/efs.py @@ -6,8 +6,9 @@ from numbers import Number from config import version as pyfaVersion from service.fit import Fit from service.market import Market -from eos.enum import Enum -from eos.saveddata.module import Hardpoint, Slot, Module, State +from eos.const import FittingModuleState, FittingHardpoint, FittingSlot +from service.const import PortEftRigSize +from eos.saveddata.module import Module from eos.saveddata.drone import Drone from eos.effectHandlerHelpers import HandledList from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup @@ -19,14 +20,6 @@ from logbook import Logger pyfalog = Logger(__name__) -class RigSize(Enum): - # Matches to item attribute "rigSize" on ship and rig items - SMALL = 1 - MEDIUM = 2 - LARGE = 3 - CAPITAL = 4 - - class EfsPort: wepTestSet = {} version = 0.03 @@ -58,13 +51,13 @@ class EfsPort: mwd50mn = mapPropData("50MN Microwarpdrive II") mwd500mn = mapPropData("500MN Microwarpdrive II") mwd50000mn = mapPropData("50000MN Microwarpdrive II") - if rigSize == RigSize.SMALL or rigSize is None: + if rigSize == PortEftRigSize.SMALL or rigSize is None: propID = mwd5mn["id"] if shipPower > mwd5mn["powerReq"] else None - elif rigSize == RigSize.MEDIUM: + elif rigSize == PortEftRigSize.MEDIUM: propID = mwd50mn["id"] if shipPower > mwd50mn["powerReq"] else mwd5mn["id"] - elif rigSize == RigSize.LARGE: + elif rigSize == PortEftRigSize.LARGE: propID = mwd500mn["id"] if shipPower > mwd500mn["powerReq"] else mwd50mn["id"] - elif rigSize == RigSize.CAPITAL: + elif rigSize == PortEftRigSize.CAPITAL: propID = mwd50000mn["id"] if shipPower > mwd50000mn["powerReq"] else mwd500mn["id"] if propID is None: @@ -86,7 +79,7 @@ class EfsPort: propWithBloom = next(filter(activePropWBloomFilter, propMods), None) if propWithBloom is not None: oldPropState = propWithBloom.state - propWithBloom.state = State.ONLINE + propWithBloom.state = FittingModuleState.ONLINE sFit.recalc(fit) sp = fit.maxSpeed sig = fit.ship.getModifiedItemAttr("signatureRadius") @@ -198,8 +191,8 @@ class EfsPort: def getModuleInfo(fit, padTypeIDs=False): moduleNames = [] modTypeIDs = [] - moduleNameSets = {Slot.LOW: [], Slot.MED: [], Slot.HIGH: [], Slot.RIG: [], Slot.SUBSYSTEM: []} - modTypeIDSets = {Slot.LOW: [], Slot.MED: [], Slot.HIGH: [], Slot.RIG: [], Slot.SUBSYSTEM: []} + moduleNameSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []} + modTypeIDSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []} for mod in fit.modules: try: if mod.item is not None: @@ -216,17 +209,17 @@ class EfsPort: pyfalog.error("Could not find name for module {0}".format(vars(mod))) for modInfo in [ - ["High Slots:"], moduleNameSets[Slot.HIGH], ["", "Med Slots:"], moduleNameSets[Slot.MED], - ["", "Low Slots:"], moduleNameSets[Slot.LOW], ["", "Rig Slots:"], moduleNameSets[Slot.RIG] + ["High Slots:"], moduleNameSets[FittingSlot.HIGH], ["", "Med Slots:"], moduleNameSets[FittingSlot.MED], + ["", "Low Slots:"], moduleNameSets[FittingSlot.LOW], ["", "Rig Slots:"], moduleNameSets[FittingSlot.RIG] ]: moduleNames.extend(modInfo) - if len(moduleNameSets[Slot.SUBSYSTEM]) > 0: + if len(moduleNameSets[FittingSlot.SUBSYSTEM]) > 0: moduleNames.extend(["", "Subsystems:"]) - moduleNames.extend(moduleNameSets[Slot.SUBSYSTEM]) + moduleNames.extend(moduleNameSets[FittingSlot.SUBSYSTEM]) - for slotType in [Slot.HIGH, Slot.MED, Slot.LOW, Slot.RIG, Slot.SUBSYSTEM]: - if slotType is not Slot.SUBSYSTEM or len(modTypeIDSets[slotType]) > 0: - modTypeIDs.extend([0, 0] if slotType is not Slot.HIGH else [0]) + for slotType in [FittingSlot.HIGH, FittingSlot.MED, FittingSlot.LOW, FittingSlot.RIG, FittingSlot.SUBSYSTEM]: + if slotType is not FittingSlot.SUBSYSTEM or len(modTypeIDSets[slotType]) > 0: + modTypeIDs.extend([0, 0] if slotType is not FittingSlot.HIGH else [0]) modTypeIDs.extend(modTypeIDSets[slotType]) droneNames = [] @@ -331,18 +324,18 @@ class EfsPort: name = stats.item.name + ", " + stats.charge.name else: name = stats.item.name - if stats.hardpoint == Hardpoint.TURRET: + if stats.hardpoint == FittingHardpoint.TURRET: tracking = stats.getModifiedItemAttr("trackingSpeed") typeing = "Turret" # Bombs share most attributes with missiles despite not needing the hardpoint - elif stats.hardpoint == Hardpoint.MISSILE or "Bomb Launcher" in stats.item.name: + elif stats.hardpoint == FittingHardpoint.MISSILE or "Bomb Launcher" in stats.item.name: maxVelocity = stats.getModifiedChargeAttr("maxVelocity") explosionDelay = stats.getModifiedChargeAttr("explosionDelay") damageReductionFactor = stats.getModifiedChargeAttr("aoeDamageReductionFactor") explosionRadius = stats.getModifiedChargeAttr("aoeCloudSize") explosionVelocity = stats.getModifiedChargeAttr("aoeVelocity") typeing = "Missile" - elif stats.hardpoint == Hardpoint.NONE: + elif stats.hardpoint == FittingHardpoint.NONE: aoeFieldRange = stats.getModifiedItemAttr("empFieldRange") # This also covers non-bomb weapons with dps values and no hardpoints, most notably targeted doomsdays. typeing = "SmartBomb" @@ -496,11 +489,11 @@ class EfsPort: getDroneMulti = lambda d: sumDamage(d.getModifiedItemAttr) * d.getModifiedItemAttr("damageMultiplier") fitMultipliers["drones"] = list(map(getDroneMulti, tf.drones)) - getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == Hardpoint.TURRET, f.modules) + getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.TURRET, f.modules) getTurretMulti = lambda mod: mod.getModifiedItemAttr("damageMultiplier") / mod.cycleTime fitMultipliers["turrets"] = list(map(getTurretMulti, getFitTurrets(tf))) - getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == Hardpoint.MISSILE, f.modules) + getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.MISSILE, f.modules) getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.cycleTime fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf))) return fitMultipliers @@ -538,7 +531,7 @@ class EfsPort: if effect._Effect__effectModule is not None: effect.handler(tf, fit.mode, []) if fit.ship.item.groupID == getGroup("Strategic Cruiser").ID: - subSystems = list(filter(lambda mod: mod.slot == Slot.SUBSYSTEM and mod.item, fit.modules)) + subSystems = list(filter(lambda mod: mod.slot == FittingSlot.SUBSYSTEM and mod.item, fit.modules)) for sub in subSystems: for effect in sub.item.effects.values(): if effect._Effect__effectModule is not None: @@ -583,7 +576,7 @@ class EfsPort: return sizeNotFoundMsg @staticmethod - def exportEfs(fit, typeNotFitFlag): + def exportEfs(fit, typeNotFitFlag, callback): sFit = Fit.getInstance() includeShipTypeData = typeNotFitFlag > 0 if includeShipTypeData: @@ -680,4 +673,8 @@ class EfsPort: pyfalog.error(e) dataDict = {"name": fitName + "Fit could not be correctly parsed"} export = json.dumps(dataDict, skipkeys=True) - return export + + if callback: + callback(export) + else: + return export diff --git a/service/port/eft.py b/service/port/eft.py index 0d8a0a706..b6c57967f 100644 --- a/service/port/eft.py +++ b/service/port/eft.py @@ -19,7 +19,6 @@ import re -from enum import Enum from logbook import Logger @@ -30,9 +29,11 @@ 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.module import Module from eos.saveddata.ship import Ship from eos.saveddata.fit import Fit +from eos.const import FittingSlot, FittingModuleState +from service.const import PortEftOptions, PortEftRigSize from service.fit import Fit as svcFit from service.market import Market from service.port.muta import parseMutant, renderMutant @@ -41,26 +42,19 @@ from service.port.shared import IPortUser, fetchItem, processing_notify pyfalog = Logger(__name__) - -class Options(Enum): - IMPLANTS = 1 - MUTATIONS = 2 - LOADED_CHARGES = 3 - - EFT_OPTIONS = ( - (Options.LOADED_CHARGES.value, 'Loaded Charges', 'Export charges loaded into modules', True), - (Options.MUTATIONS.value, 'Mutated Attributes', 'Export mutated modules\' stats', True), - (Options.IMPLANTS.value, 'Implants && Boosters', 'Export implants and boosters', True), + (PortEftOptions.LOADED_CHARGES, 'Loaded Charges', 'Export charges loaded into modules', True), + (PortEftOptions.MUTATIONS, 'Mutated Attributes', 'Export mutated modules\' stats', True), + (PortEftOptions.IMPLANTS, 'Implants && Boosters', 'Export implants and boosters', True), ) MODULE_CATS = ('Module', 'Subsystem', 'Structure Module') -SLOT_ORDER = (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE) +SLOT_ORDER = (FittingSlot.LOW, FittingSlot.MED, FittingSlot.HIGH, FittingSlot.RIG, FittingSlot.SUBSYSTEM, FittingSlot.SERVICE) OFFLINE_SUFFIX = '/OFFLINE' -def exportEft(fit, options): +def exportEft(fit, options, callback): # 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 @@ -86,21 +80,21 @@ def exportEft(fit, options): modName = module.baseItem.name else: modName = module.item.name - if module.isMutated and options[Options.MUTATIONS.value]: + if module.isMutated and options[PortEftOptions.MUTATIONS]: mutants[mutantReference] = module mutationSuffix = ' [{}]'.format(mutantReference) mutantReference += 1 else: mutationSuffix = '' - modOfflineSuffix = ' {}'.format(OFFLINE_SUFFIX) if module.state == State.OFFLINE else '' - if module.charge and options[Options.LOADED_CHARGES.value]: + modOfflineSuffix = ' {}'.format(OFFLINE_SUFFIX) if module.state == FittingModuleState.OFFLINE else '' + if module.charge and options[PortEftOptions.LOADED_CHARGES]: 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 '')) + FittingSlot(slotType).name.capitalize() if slotType is not None else '')) if rackLines: modSection.append('\n'.join(rackLines)) if modSection: @@ -122,15 +116,15 @@ def exportEft(fit, options): sections.append('\n\n'.join(minionSection)) # Section 3: implants, boosters - if options[Options.IMPLANTS.value]: + if options[PortEftOptions.IMPLANTS]: charSection = [] implantLines = [] - for implant in fit.implants: + for implant in sorted(fit.implants, key=lambda i: i.slot or 0): implantLines.append(implant.item.name) if implantLines: charSection.append('\n'.join(implantLines)) boosterLines = [] - for booster in fit.boosters: + for booster in sorted(fit.boosters, key=lambda b: b.slot or 0): boosterLines.append(booster.item.name) if boosterLines: charSection.append('\n'.join(boosterLines)) @@ -149,14 +143,19 @@ def exportEft(fit, options): # Section 5: mutated modules' details mutationLines = [] - if mutants and options[Options.MUTATIONS.value]: + if mutants and options[PortEftOptions.MUTATIONS]: for mutantReference in sorted(mutants): mutant = mutants[mutantReference] mutationLines.append(renderMutant(mutant, firstPrefix='[{}] '.format(mutantReference), prefix=' ')) if mutationLines: sections.append('\n'.join(mutationLines)) - return '{}\n\n{}'.format(header, '\n\n\n'.join(sections)) + text = '{}\n\n{}'.format(header, '\n\n\n'.join(sections)) + + if callback: + callback(text) + else: + return text def importEft(lines): @@ -441,8 +440,8 @@ def importEftCfg(shipname, lines, iportuser): else: m.owner = fitobj # Activate mod if it is activable - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE + if m.isValidState(FittingModuleState.ACTIVE): + m.state = FittingModuleState.ACTIVE # Add charge to mod if applicable, on any errors just don't add anything if chargeName: try: @@ -722,12 +721,12 @@ class AbstractFit: @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} + FittingSlot.HIGH: self.modulesHigh, + FittingSlot.MED: self.modulesMed, + FittingSlot.LOW: self.modulesLow, + FittingSlot.RIG: self.rigs, + FittingSlot.SUBSYSTEM: self.subsystems, + FittingSlot.SERVICE: self.services} def getContainerBySlot(self, slotType): return self.__slotContainerMap.get(slotType) @@ -798,10 +797,10 @@ class AbstractFit: 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 + if itemSpec.offline and m.isValidState(FittingModuleState.OFFLINE): + m.state = FittingModuleState.OFFLINE + elif m.isValidState(FittingModuleState.ACTIVE): + m.state = FittingModuleState.ACTIVE return m def addImplant(self, itemSpec): diff --git a/service/port/esi.py b/service/port/esi.py index a97480624..b97e9d20a 100644 --- a/service/port/esi.py +++ b/service/port/esi.py @@ -28,7 +28,8 @@ 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.module import Module +from eos.const import FittingSlot, FittingModuleState from eos.saveddata.ship import Ship from service.fit import Fit as svcFit from service.market import Market @@ -41,12 +42,12 @@ class ESIExportException(Exception): pyfalog = Logger(__name__) INV_FLAGS = { - Slot.LOW: 11, - Slot.MED: 19, - Slot.HIGH: 27, - Slot.RIG: 92, - Slot.SUBSYSTEM: 125, - Slot.SERVICE: 164 + FittingSlot.LOW: 11, + FittingSlot.MED: 19, + FittingSlot.HIGH: 27, + FittingSlot.RIG: 92, + FittingSlot.SUBSYSTEM: 125, + FittingSlot.SERVICE: 164 } INV_FLAG_CARGOBAY = 5 @@ -54,7 +55,7 @@ INV_FLAG_DRONEBAY = 87 INV_FLAG_FIGHTER = 158 -def exportESI(ofit): +def exportESI(ofit, callback): # A few notes: # max fit name length is 50 characters # Most keys are created simply because they are required, but bogus data is okay @@ -82,7 +83,7 @@ def exportESI(ofit): item = nested_dict() slot = module.slot - if slot == Slot.SUBSYSTEM: + if slot == FittingSlot.SUBSYSTEM: # Order of subsystem matters based on this attr. See GH issue #130 slot = int(module.getModifiedItemAttr("subSystemSlot")) item['flag'] = slot @@ -134,7 +135,12 @@ def exportESI(ofit): if len(fit['items']) == 0: raise ESIExportException("Cannot export fitting: module list cannot be empty.") - return json.dumps(fit) + text = json.dumps(fit) + + if callback: + callback(text) + else: + return text def importESI(string): @@ -189,8 +195,8 @@ def importESI(string): if m.fits(fitobj): fitobj.modules.append(m) else: - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE + if m.isValidState(FittingModuleState.ACTIVE): + m.state = FittingModuleState.ACTIVE moduleList.append(m) diff --git a/service/port/multibuy.py b/service/port/multibuy.py index 1c6b78836..2dfe7eff8 100644 --- a/service/port/multibuy.py +++ b/service/port/multibuy.py @@ -18,60 +18,77 @@ # ============================================================================= -from enum import Enum - - -class Options(Enum): - IMPLANTS = 1 - CARGO = 2 - LOADED_CHARGES = 3 +from service.const import PortMultiBuyOptions +from service.price import Price as sPrc MULTIBUY_OPTIONS = ( - (Options.LOADED_CHARGES.value, 'Loaded Charges', 'Export charges loaded into modules', True), - (Options.IMPLANTS.value, 'Implants && Boosters', 'Export implants and boosters', False), - (Options.CARGO.value, 'Cargo', 'Export cargo contents', True), + (PortMultiBuyOptions.LOADED_CHARGES, 'Loaded Charges', 'Export charges loaded into modules', True), + (PortMultiBuyOptions.IMPLANTS, 'Implants && Boosters', 'Export implants and boosters', False), + (PortMultiBuyOptions.CARGO, 'Cargo', 'Export cargo contents', True), + (PortMultiBuyOptions.OPTIMIZE_PRICES, 'Optimize Prices', 'Replace items by cheaper alternatives', False), ) -def exportMultiBuy(fit, options): - itemCounts = {} - - def addItem(item, quantity=1): - if item not in itemCounts: - itemCounts[item] = 0 - itemCounts[item] += quantity +def exportMultiBuy(fit, options, callback): + itemAmounts = {} for module in fit.modules: if module.item: # Mutated items are of no use for multibuy if module.isMutated: continue - addItem(module.item) - if module.charge and options[Options.LOADED_CHARGES.value]: - addItem(module.charge, module.numCharges) + _addItem(itemAmounts, module.item) + if module.charge and options[PortMultiBuyOptions.LOADED_CHARGES]: + _addItem(itemAmounts, module.charge, module.numCharges) for drone in fit.drones: - addItem(drone.item, drone.amount) + _addItem(itemAmounts, drone.item, drone.amount) for fighter in fit.fighters: - addItem(fighter.item, fighter.amountActive) + _addItem(itemAmounts, fighter.item, fighter.amountActive) - if options[Options.CARGO.value]: + if options[PortMultiBuyOptions.CARGO]: for cargo in fit.cargo: - addItem(cargo.item, cargo.amount) + _addItem(itemAmounts, cargo.item, cargo.amount) - if options[Options.IMPLANTS.value]: + if options[PortMultiBuyOptions.IMPLANTS]: for implant in fit.implants: - addItem(implant.item) + _addItem(itemAmounts, implant.item) for booster in fit.boosters: - addItem(booster.item) + _addItem(itemAmounts, booster.item) + if options[PortMultiBuyOptions.OPTIMIZE_PRICES]: + + def formatCheaperExportCb(replacementsCheaper): + updatedAmounts = {} + for item, itemAmount in itemAmounts.items(): + _addItem(updatedAmounts, replacementsCheaper.get(item, item), itemAmount) + string = _prepareString(fit.ship.item, updatedAmounts) + callback(string) + + priceSvc = sPrc.getInstance() + priceSvc.findCheaperReplacements(itemAmounts, formatCheaperExportCb) + else: + string = _prepareString(fit.ship.item, itemAmounts) + if callback: + callback(string) + else: + return string + + +def _addItem(container, item, quantity=1): + if item not in container: + container[item] = 0 + container[item] += quantity + + +def _prepareString(shipItem, itemAmounts): exportLines = [] - exportLines.append(fit.ship.item.name) - for item in sorted(itemCounts, key=lambda i: (i.group.category.name, i.group.name, i.name)): - count = itemCounts[item] + exportLines.append(shipItem.name) + for item in sorted(itemAmounts, key=lambda i: (i.group.category.name, i.group.name, i.name)): + count = itemAmounts[item] if count == 1: exportLines.append(item.name) else: diff --git a/service/port/port.py b/service/port/port.py index 8038cab8e..1f961f6c1 100644 --- a/service/port/port.py +++ b/service/port/port.py @@ -29,7 +29,7 @@ from bs4 import UnicodeDammit from logbook import Logger from eos import db -from eos.saveddata.fit import ImplantLocation +from eos.const import ImplantLocation from service.fit import Fit as svcFit from service.port.dna import exportDna, importDna from service.port.eft import exportEft, importEft, importEftCfg @@ -257,8 +257,8 @@ class Port(object): return importEftCfg(shipname, lines, iportuser) @classmethod - def exportEft(cls, fit, options): - return exportEft(fit, options) + def exportEft(cls, fit, options, callback=None): + return exportEft(fit, options, callback=callback) # DNA-related methods @staticmethod @@ -266,8 +266,8 @@ class Port(object): return importDna(string) @staticmethod - def exportDna(fit): - return exportDna(fit) + def exportDna(fit, callback=None): + return exportDna(fit, callback=callback) # ESI-related methods @staticmethod @@ -275,8 +275,8 @@ class Port(object): return importESI(string) @staticmethod - def exportESI(fit): - return exportESI(fit) + def exportESI(fit, callback=None): + return exportESI(fit, callback=callback) # XML-related methods @staticmethod @@ -284,10 +284,10 @@ class Port(object): return importXml(text, iportuser) @staticmethod - def exportXml(iportuser=None, *fits): - return exportXml(iportuser, *fits) + def exportXml(iportuser=None, callback=None, *fits): + return exportXml(iportuser, callback=callback, *fits) # Multibuy-related methods @staticmethod - def exportMultiBuy(fit, options): - return exportMultiBuy(fit, options) + def exportMultiBuy(fit, options, callback=None): + return exportMultiBuy(fit, options, callback=callback) diff --git a/service/port/xml.py b/service/port/xml.py index 432bbcdd8..0e1f1e9ed 100644 --- a/service/port/xml.py +++ b/service/port/xml.py @@ -28,8 +28,9 @@ 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.module import Module from eos.saveddata.ship import Ship +from eos.const import FittingSlot, FittingModuleState from service.fit import Fit as svcFit from service.market import Market from utils.strfunctions import sequential_rep, replace_ltgt @@ -198,8 +199,8 @@ def importXml(text, iportuser): m.owner = fitobj fitobj.modules.append(m) else: - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE + if m.isValidState(FittingModuleState.ACTIVE): + m.state = FittingModuleState.ACTIVE moduleList.append(m) @@ -225,14 +226,13 @@ def importXml(text, iportuser): return fit_list -def exportXml(iportuser, *fits): +def exportXml(iportuser, callback, *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: @@ -266,7 +266,7 @@ def exportXml(iportuser, *fits): slot = module.slot - if slot == Slot.SUBSYSTEM: + if slot == FittingSlot.SUBSYSTEM: # Order of subsystem matters based on this attr. See GH issue #130 slotId = module.getModifiedItemAttr("subSystemSlot") - 125 else: @@ -278,7 +278,7 @@ def exportXml(iportuser, *fits): hardware = doc.createElement("hardware") hardware.setAttribute("type", module.item.name) - slotName = Slot.getName(slot).lower() + slotName = FittingSlot(slot).name.lower() slotName = slotName if slotName != "high" else "hi" hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId)) fitting.appendChild(hardware) @@ -323,4 +323,9 @@ def exportXml(iportuser, *fits): iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE, (i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name)) ) - return doc.toprettyxml() + text = doc.toprettyxml() + + if callback: + callback(text) + else: + return text diff --git a/service/price.py b/service/price.py index 3028b74ad..5cfdf09c8 100644 --- a/service/price.py +++ b/service/price.py @@ -18,28 +18,26 @@ # ============================================================================= +import math import queue import threading -import time +from itertools import chain import wx from logbook import Logger from eos import db from eos.saveddata.price import PriceStatus +from gui.fitCommands.guiRebaseItems import GuiRebaseItemsCommand from service.fit import Fit from service.market import Market from service.network import TimeoutError + pyfalog = Logger(__name__) -VALIDITY = 24 * 60 * 60 # Price validity period, 24 hours -REREQUEST = 4 * 60 * 60 # Re-request delay for failed fetches, 4 hours -TIMEOUT = 15 * 60 # Network timeout delay for connection issues, 15 minutes - - -class Price(object): +class Price: instance = None systemsList = { @@ -69,38 +67,34 @@ class Price(object): return cls.instance @classmethod - def fetchPrices(cls, prices): + def fetchPrices(cls, prices, fetchTimeout, validityOverride): """Fetch all prices passed to this method""" # Dictionary for our price objects priceMap = {} - # Check all provided price objects, and add invalid ones to dictionary + # Check all provided price objects, and add those we want to update to + # dictionary for price in prices: - if not price.isValid: + if not price.isValid(validityOverride): priceMap[price.typeID] = price - if len(priceMap) == 0: + if not priceMap: return - # Set of items which are still to be requested from this service - toRequest = set() - # Compose list of items we're going to request for typeID in tuple(priceMap): # Get item object item = db.getItem(typeID) - # We're not going to request items only with market group, as eve-central - # doesn't provide any data for items not on the market + # We're not going to request items only with market group, as our current market + # sources do not provide any data for items not on the market if item is None: continue if not item.marketGroupID: - priceMap[typeID].status = PriceStatus.notSupported + priceMap[typeID].update(PriceStatus.notSupported) del priceMap[typeID] continue - toRequest.add(typeID) - # Do not waste our time if all items are not on the market - if len(toRequest) == 0: + if not priceMap: return sFit = Fit.getInstance() @@ -110,62 +104,46 @@ class Price(object): return # attempt to find user's selected price source, otherwise get first one - sourcesToTry = list(cls.sources.keys()) - curr = sFit.serviceFittingOptions["priceSource"] if sFit.serviceFittingOptions["priceSource"] in sourcesToTry else sourcesToTry[0] + sourceAll = list(cls.sources.keys()) + sourcePrimary = sFit.serviceFittingOptions["priceSource"] if sFit.serviceFittingOptions["priceSource"] in sourceAll else sourceAll[0] - while len(sourcesToTry) > 0: - sourcesToTry.remove(curr) + # Format: {source name: timeout weight} + sources = {sourcePrimary: len(sourceAll)} + for source in sourceAll: + if source == sourcePrimary: + continue + sources[source] = min(sources.values()) - 1 + timeoutWeightMult = fetchTimeout / sum(sources.values()) + + # Record timeouts as it will affect our final decision + timedOutSources = {} + + for source, timeoutWeight in sources.items(): + pyfalog.info('Trying {}'.format(source)) + timedOutSources[source] = False + sourceFetchTimeout = timeoutWeight * timeoutWeightMult try: - sourceCls = cls.sources.get(curr) - sourceCls(toRequest, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], priceMap) - break - # If getting or processing data returned any errors + sourceCls = cls.sources.get(source) + sourceCls(priceMap, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], sourceFetchTimeout) except TimeoutError: - # Timeout error deserves special treatment - pyfalog.warning("Price fetch timout") - for typeID in tuple(priceMap): - priceobj = priceMap[typeID] - priceobj.time = time.time() + TIMEOUT - priceobj.status = PriceStatus.fail - del priceMap[typeID] - except Exception as ex: - # something happened, try another source - pyfalog.warn('Failed to fetch prices from price source {}: {}'.format(curr, ex, sourcesToTry[0])) - if len(sourcesToTry) > 0: - pyfalog.warn('Trying {}'.format(sourcesToTry[0])) - curr = sourcesToTry[0] + pyfalog.warning("Price fetch timeout for source {}".format(source)) + timedOutSources[source] = True + except Exception as e: + pyfalog.warn('Failed to fetch prices from price source {}: {}'.format(source, e)) + # Sources remove price map items as they fetch info, if none remain then we're done + if not priceMap: + break - # if we get to this point, then we've got an error in all of our sources. Set to REREQUEST delay - for typeID in priceMap.keys(): - priceobj = priceMap[typeID] - priceobj.time = time.time() + REREQUEST - priceobj.status = PriceStatus.fail - - @classmethod - def fitItemsList(cls, fit): - # Compose a list of all the data we need & request it - fit_items = [fit.ship.item] - - for mod in fit.modules: - if not mod.isEmpty: - fit_items.append(mod.item) - - for drone in fit.drones: - fit_items.append(drone.item) - - for fighter in fit.fighters: - fit_items.append(fighter.item) - - for cargo in fit.cargo: - fit_items.append(cargo.item) - - for boosters in fit.boosters: - fit_items.append(boosters.item) - - for implants in fit.implants: - fit_items.append(implants.item) - - return list(set(fit_items)) + # If we get to this point, then we've failed to get price with all our sources + # If all sources failed due to timeouts, set one status + if all(to is True for to in timedOutSources.values()): + for typeID in priceMap.keys(): + priceMap[typeID].update(PriceStatus.fetchTimeout) + # If some sources failed due to any other reason, then it's definitely not network + # timeout and we just set another status + else: + for typeID in priceMap.keys(): + priceMap[typeID].update(PriceStatus.fetchFail) def getPriceNow(self, objitem): """Get price for provided typeID""" @@ -174,7 +152,7 @@ class Price(object): return item.price.price - def getPrices(self, objitems, callback, waitforthread=False): + def getPrices(self, objitems, callback, fetchTimeout=30, waitforthread=False, validityOverride=None): """Get prices for multiple typeIDs""" requests = [] sMkt = Market.getInstance() @@ -186,7 +164,7 @@ class Price(object): try: callback(requests) except Exception as e: - pyfalog.critical("Callback failed.") + pyfalog.critical("Execution of callback from getPrices failed.") pyfalog.critical(e) db.commit() @@ -194,12 +172,43 @@ class Price(object): if waitforthread: self.priceWorkerThread.setToWait(requests, cb) else: - self.priceWorkerThread.trigger(requests, cb) + self.priceWorkerThread.trigger(requests, cb, fetchTimeout, validityOverride) def clearPriceCache(self): pyfalog.debug("Clearing Prices") db.clearPrices() + def findCheaperReplacements(self, items, callback, fetchTimeout=10): + sMkt = Market.getInstance() + + replacementsAll = {} # All possible item replacements + for item in items: + if item in replacementsAll: + continue + itemRepls = sMkt.getReplacements(item) + if itemRepls: + replacementsAll[item] = itemRepls + itemsToFetch = {i for i in chain(replacementsAll.keys(), *replacementsAll.values())} + + def makeCheapMapCb(requests): + # Decide what we are going to replace + replacementsCheaper = {} # Items which should be replaced + for replacee, replacers in replacementsAll.items(): + replacer = min(replacers, key=lambda i: i.price.price or math.inf) + if (replacer.price.price or math.inf) < (replacee.price.price or math.inf): + replacementsCheaper[replacee] = replacer + try: + callback(replacementsCheaper) + except Exception as e: + pyfalog.critical("Execution of callback from findCheaperReplacements failed.") + pyfalog.critical(e) + + # Prices older than 2 hours have to be refetched + validityOverride = 2 * 60 * 60 + self.getPrices(itemsToFetch, makeCheapMapCb, fetchTimeout=fetchTimeout, validityOverride=validityOverride) + + + class PriceWorkerThread(threading.Thread): @@ -214,11 +223,11 @@ class PriceWorkerThread(threading.Thread): queue = self.queue while True: # Grab our data - callback, requests = queue.get() + callback, requests, fetchTimeout, validityOverride = queue.get() # Grab prices, this is the time-consuming part if len(requests) > 0: - Price.fetchPrices(requests) + Price.fetchPrices(requests, fetchTimeout, validityOverride) wx.CallAfter(callback) queue.task_done() @@ -230,14 +239,13 @@ class PriceWorkerThread(threading.Thread): for callback in callbacks: wx.CallAfter(callback) - def trigger(self, prices, callbacks): - self.queue.put((callbacks, prices)) + def trigger(self, prices, callbacks, fetchTimeout, validityOverride): + self.queue.put((callbacks, prices, fetchTimeout, validityOverride)) def setToWait(self, prices, callback): - for x in prices: - if x.typeID not in self.wait: - self.wait[x.typeID] = [] - self.wait[x.typeID].append(callback) + for price in prices: + callbacks = self.wait.setdefault(price.typeID, []) + callbacks.append(callback) # Import market sources only to initialize price source modules, they register on their own diff --git a/service/settings.py b/service/settings.py index df5038d1f..b901b1d72 100644 --- a/service/settings.py +++ b/service/settings.py @@ -526,6 +526,7 @@ class ContextMenuSettings(object): "targetResists" : 1, "whProjector" : 1, "moduleFill" : 1, + "spoolup" : 1, } self.ContextMenuDefaultSettings = SettingsProvider.getInstance().getSettings("pyfaContextMenuSettings", ContextMenuDefaultSettings)