diff --git a/README.md b/README.md index a3b795749..5700287f8 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ If you wish to help with development or simply need to run pyfa through a Python * `dateutil` * `matplotlib` (for some Linux distributions you may need to install separate wxPython bindings such as `python-matplotlib-wx`) * `requests` +* `logbook` >= 1.0.0 ## Bug Reporting The preferred method of reporting bugs is through the project's [GitHub Issues interface](https://github.com/pyfa-org/Pyfa/issues). Alternatively, posting a report in the [pyfa thread](http://forums.eveonline.com/default.aspx?g=posts&t=247609) on the official EVE Online forums is acceptable. Guidelines for bug reporting can be found on [this wiki page](https://github.com/DarkFenX/Pyfa/wiki/Bug-Reporting). diff --git a/_development/__init__.py b/_development/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/_development/helpers.py b/_development/helpers.py new file mode 100644 index 000000000..b35ac835f --- /dev/null +++ b/_development/helpers.py @@ -0,0 +1,143 @@ +# noinspection PyPackageRequirements +import pytest + +import os +import sys +import threading + +from sqlalchemy import MetaData, create_engine +from sqlalchemy.orm import sessionmaker + +script_dir = os.path.dirname(os.path.abspath(__file__)) +# Add root folder to python paths +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..'))) +sys._called_from_test = True + +# noinspection PyUnresolvedReferences,PyUnusedLocal +@pytest.fixture +def DBInMemory_test(): + def rollback(): + with sd_lock: + saveddata_session.rollback() + + + print("Creating database in memory") + from os.path import realpath, join, dirname, abspath + + debug = False + gamedataCache = True + saveddataCache = True + gamedata_version = "" + gamedata_connectionstring = 'sqlite:///' + realpath(join(dirname(abspath(unicode(__file__))), "..", "eve.db")) + saveddata_connectionstring = 'sqlite:///:memory:' + + class ReadOnlyException(Exception): + pass + + if callable(gamedata_connectionstring): + gamedata_engine = create_engine("sqlite://", creator=gamedata_connectionstring, echo=debug) + else: + gamedata_engine = create_engine(gamedata_connectionstring, echo=debug) + + gamedata_meta = MetaData() + gamedata_meta.bind = gamedata_engine + gamedata_session = sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False)() + + # This should be moved elsewhere, maybe as an actual query. Current, without try-except, it breaks when making a new + # game db because we haven't reached gamedata_meta.create_all() + try: + gamedata_version = gamedata_session.execute( + "SELECT `field_value` FROM `metadata` WHERE `field_name` LIKE 'client_build'" + ).fetchone()[0] + except Exception as e: + print("Missing gamedata version.") + gamedata_version = None + + if saveddata_connectionstring is not None: + if callable(saveddata_connectionstring): + saveddata_engine = create_engine(creator=saveddata_connectionstring, echo=debug) + else: + saveddata_engine = create_engine(saveddata_connectionstring, echo=debug) + + saveddata_meta = MetaData() + saveddata_meta.bind = saveddata_engine + saveddata_session = sessionmaker(bind=saveddata_engine, autoflush=False, expire_on_commit=False)() + else: + saveddata_meta = None + + # Lock controlling any changes introduced to session + sd_lock = threading.Lock() + + # Import all the definitions for all our database stuff + # noinspection PyPep8 + #from eos.db.gamedata import alphaClones, attribute, category, effect, group, icon, item, marketGroup, metaData, metaGroup, queries, traits, unit + # noinspection PyPep8 + #from eos.db.saveddata import booster, cargo, character, crest, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, miscData, module, override, price, queries, skill, targetResists, user + + # If using in memory saveddata, you'll want to reflect it so the data structure is good. + if saveddata_connectionstring == "sqlite:///:memory:": + saveddata_meta.create_all() + + # Output debug info to help us troubleshoot Travis + print(saveddata_engine) + print(gamedata_engine) + + helper = { + #'config': eos.config, + 'gamedata_session' : gamedata_session, + 'saveddata_session' : saveddata_session, + } + return helper + +# noinspection PyUnresolvedReferences,PyUnusedLocal +@pytest.fixture +def DBInMemory(): + print("Creating database in memory") + + import eos.config + + import eos + import eos.db + + # Output debug info to help us troubleshoot Travis + print(eos.db.saveddata_engine) + print(eos.db.gamedata_engine) + + helper = { + 'config': eos.config, + 'db' : eos.db, + 'gamedata_session' : eos.db.gamedata_session, + 'saveddata_session' : eos.db.saveddata_session, + } + return helper + + +@pytest.fixture +def Gamedata(): + print("Building Gamedata") + from eos.gamedata import Item + + helper = { + 'Item': Item, + } + return helper + + +@pytest.fixture +def Saveddata(): + print("Building 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.citadel import Citadel + + helper = { + 'Structure': Citadel, + 'Ship' : Ship, + 'Fit' : Fit, + 'Character': Character, + 'Module' : Module, + 'State' : State, + } + return helper diff --git a/_development/helpers_fits.py b/_development/helpers_fits.py new file mode 100644 index 000000000..0e41b9f6e --- /dev/null +++ b/_development/helpers_fits.py @@ -0,0 +1,28 @@ +import pytest + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata + + +# noinspection PyShadowingNames +@pytest.fixture +def RifterFit(DB, Gamedata, Saveddata): + print("Creating Rifter") + item = DB['gamedata_session'].query(Gamedata['Item']).filter(Gamedata['Item'].name == "Rifter").first() + ship = Saveddata['Ship'](item) + # setup fit + fit = Saveddata['Fit'](ship, "My Rifter Fit") + + return fit + + +# noinspection PyShadowingNames +@pytest.fixture +def KeepstarFit(DB, Gamedata, Saveddata): + print("Creating Keepstar") + item = DB['gamedata_session'].query(Gamedata['Item']).filter(Gamedata['Item'].name == "Keepstar").first() + ship = Saveddata['Structure'](item) + # setup fit + fit = Saveddata['Fit'](ship, "Keepstar Fit") + + return fit diff --git a/config.py b/config.py index e3bfad025..884803d76 100644 --- a/config.py +++ b/config.py @@ -19,10 +19,10 @@ debug = False saveInRoot = False # Version data -version = "1.27.3" +version = "1.28.2" tag = "git" -expansionName = "YC119.2" -expansionVersion = "1.4" +expansionName = "YC119.3" +expansionVersion = "1.0" evemonMinVersion = "4081" pyfaPath = None diff --git a/eos/config.py b/eos/config.py index 38371e299..9dc0eabbc 100644 --- a/eos/config.py +++ b/eos/config.py @@ -1,14 +1,25 @@ import sys from os.path import realpath, join, dirname, abspath +from logbook import Logger +import os +istravis = os.environ.get('TRAVIS') == 'true' +pyfalog = Logger(__name__) + debug = False gamedataCache = True saveddataCache = True gamedata_version = "" -gamedata_connectionstring = 'sqlite:///' + unicode(realpath(join(dirname(abspath(__file__)), "..", "eve.db")), - sys.getfilesystemencoding()) -saveddata_connectionstring = 'sqlite:///' + unicode( - realpath(join(dirname(abspath(__file__)), "..", "saveddata", "saveddata.db")), sys.getfilesystemencoding()) +gamedata_connectionstring = 'sqlite:///' + unicode(realpath(join(dirname(abspath(__file__)), "..", "eve.db")), sys.getfilesystemencoding()) +pyfalog.debug("Gamedata connection string: {0}", gamedata_connectionstring) + +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:///' + unicode(realpath(join(dirname(abspath(__file__)), "..", "saveddata", "saveddata.db")), sys.getfilesystemencoding()) + +pyfalog.debug("Saveddata connection string: {0}", saveddata_connectionstring) settings = { "setting1": True diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py index 757d8a3d1..9658d6855 100644 --- a/eos/modifiedAttributeDict.py +++ b/eos/modifiedAttributeDict.py @@ -19,6 +19,9 @@ import collections from math import exp +# TODO: This needs to be moved out, we shouldn't have *ANY* dependencies back to other modules/methods inside eos. +# This also breaks writing any tests. :( +from eos.db.gamedata.queries import getAttributeInfo defaultValuesCache = {} cappingAttrKeyCache = {} @@ -26,22 +29,26 @@ cappingAttrKeyCache = {} class ItemAttrShortcut(object): def getModifiedItemAttr(self, key, default=None): - if key in self.itemModifiedAttributes: - return self.itemModifiedAttributes[key] - else: - return default + return_value = self.itemModifiedAttributes.get(key) + + if return_value is None and default is not None: + return_value = default + + return return_value class ChargeAttrShortcut(object): def getModifiedChargeAttr(self, key, default=None): - if key in self.chargeModifiedAttributes: - return self.chargeModifiedAttributes[key] - else: - return default + return_value = self.chargeModifiedAttributes.get(key) + + if return_value is None and default is not None: + return_value = default + + return return_value class ModifiedAttributeDict(collections.MutableMapping): - OVERRIDES = False + overrides_enabled = False class CalculationPlaceholder(object): def __init__(self): @@ -98,15 +105,23 @@ class ModifiedAttributeDict(collections.MutableMapping): def __getitem__(self, key): # Check if we have final calculated value - if key in self.__modified: - if self.__modified[key] == self.CalculationPlaceholder: - self.__modified[key] = self.__calculateValue(key) - return self.__modified[key] + key_value = self.__modified.get(key) + if key_value is self.CalculationPlaceholder: + key_value = self.__modified[key] = self.__calculateValue(key) + + if key_value is not None: + return key_value + # Then in values which are not yet calculated - elif key in self.__intermediary: - return self.__intermediary[key] - # Original value is the least priority + if self.__intermediary: + val = self.__intermediary.get(key) else: + val = None + + if val is not None: + return val + else: + # Original value is the least priority return self.getOriginal(key) def __delitem__(self, key): @@ -115,12 +130,18 @@ class ModifiedAttributeDict(collections.MutableMapping): if key in self.__intermediary: del self.__intermediary[key] - def getOriginal(self, key): - if self.OVERRIDES and key in self.__overrides: - return self.__overrides.get(key).value - val = self.__original.get(key) + def getOriginal(self, key, default=None): + if self.overrides_enabled and self.overrides: + val = self.overrides.get(key, None) + else: + val = None + if val is None: - return None + if self.original: + val = self.original.get(key, None) + + if val is None and val != default: + val = default return val.value if hasattr(val, "value") else val @@ -128,12 +149,12 @@ class ModifiedAttributeDict(collections.MutableMapping): self.__intermediary[key] = val def __iter__(self): - all = dict(self.__original, **self.__modified) - return (key for key in all) + all_dict = dict(self.original, **self.__modified) + return (key for key in all_dict) def __contains__(self, key): - return (self.__original is not None and key in self.__original) or \ - key in self.__modified or key in self.__intermediary + return (self.original is not None and key in self.original) or \ + key in self.__modified or key in self.__intermediary def __placehold(self, key): """Create calculation placeholder in item's modified attribute dict""" @@ -141,7 +162,7 @@ class ModifiedAttributeDict(collections.MutableMapping): def __len__(self): keys = set() - keys.update(self.__original.iterkeys()) + keys.update(self.original.iterkeys()) keys.update(self.__modified.iterkeys()) keys.update(self.__intermediary.iterkeys()) return len(keys) @@ -152,7 +173,6 @@ class ModifiedAttributeDict(collections.MutableMapping): try: cappingKey = cappingAttrKeyCache[key] except KeyError: - from eos.db.gamedata.queries import getAttributeInfo attrInfo = getAttributeInfo(key) if attrInfo is None: cappingId = cappingAttrKeyCache[key] = None @@ -166,12 +186,8 @@ class ModifiedAttributeDict(collections.MutableMapping): cappingKey = None if cappingAttrInfo is None else cappingAttrInfo.name if cappingKey: - if cappingKey in self.original: - # some items come with their own caps (ie: carriers). If they do, use this - cappingValue = self.original.get(cappingKey).value - else: - # If not, get info about the default value - cappingValue = self.__calculateValue(cappingKey) + cappingValue = self.original.get(cappingKey, self.__calculateValue(cappingKey)) + cappingValue = cappingValue.value if hasattr(cappingValue, "value") else cappingValue else: cappingValue = None @@ -183,25 +199,28 @@ class ModifiedAttributeDict(collections.MutableMapping): force = min(force, cappingValue) return force # Grab our values if they're there, otherwise we'll take default values - preIncrease = self.__preIncreases[key] if key in self.__preIncreases else 0 - multiplier = self.__multipliers[key] if key in self.__multipliers else 1 - penalizedMultiplierGroups = self.__penalizedMultipliers[key] if key in self.__penalizedMultipliers else {} - postIncrease = self.__postIncreases[key] if key in self.__postIncreases else 0 + preIncrease = self.__preIncreases.get(key, 0) + multiplier = self.__multipliers.get(key, 1) + penalizedMultiplierGroups = self.__penalizedMultipliers.get(key, {}) + postIncrease = self.__postIncreases.get(key, 0) # Grab initial value, priorities are: # Results of ongoing calculation > preAssign > original > 0 try: default = defaultValuesCache[key] except KeyError: - from eos.db.gamedata.queries import getAttributeInfo attrInfo = getAttributeInfo(key) if attrInfo is None: default = defaultValuesCache[key] = 0.0 else: dv = attrInfo.defaultValue default = defaultValuesCache[key] = dv if dv is not None else 0.0 - val = self.__intermediary[key] if key in self.__intermediary else self.__preAssigns[ - key] if key in self.__preAssigns else self.getOriginal(key) if key in self.__original else default + + val = self.__intermediary.get(key, + self.__preAssigns.get(key, + self.getOriginal(key, default) + ) + ) # We'll do stuff in the following order: # preIncrease > multiplier > stacking penalized multipliers > postIncrease @@ -254,7 +273,7 @@ class ModifiedAttributeDict(collections.MutableMapping): return skill.level def getAfflictions(self, key): - return self.__affectedBy[key] if key in self.__affectedBy else {} + return self.__affectedBy.get(key, {}) def iterAfflictions(self): return self.__affectedBy.__iter__() @@ -360,6 +379,6 @@ class ModifiedAttributeDict(collections.MutableMapping): class Affliction(object): - def __init__(self, type, amount): - self.type = type + def __init__(self, affliction_type, amount): + self.type = affliction_type self.amount = amount diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 3338e8351..5f2695710 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -128,9 +128,9 @@ class Fit(object): self.__capRecharge = None self.__calculatedTargets = [] self.__remoteReps = { - "Armor": None, - "Shield": None, - "Hull": None, + "Armor" : None, + "Shield" : None, + "Hull" : None, "Capacitor": None, } self.factorReload = False @@ -370,9 +370,11 @@ class Fit(object): @validates("ID", "ownerID", "shipID") def validator(self, key, val): - map = {"ID": lambda _val: isinstance(_val, int), - "ownerID": lambda _val: isinstance(_val, int) or _val is None, - "shipID": lambda _val: isinstance(_val, int) or _val is None} + map = { + "ID" : lambda _val: isinstance(_val, int), + "ownerID": lambda _val: isinstance(_val, int) or _val is None, + "shipID" : lambda _val: isinstance(_val, int) or _val is None + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) @@ -408,15 +410,15 @@ class Fit(object): self.ship.clear() c = chain( - self.modules, - self.drones, - self.fighters, - self.boosters, - self.implants, - self.projectedDrones, - self.projectedModules, - self.projectedFighters, - (self.character, self.extraAttributes), + self.modules, + self.drones, + self.fighters, + self.boosters, + self.implants, + self.projectedDrones, + self.projectedModules, + self.projectedFighters, + (self.character, self.extraAttributes), ) for stuff in c: @@ -476,11 +478,11 @@ class Fit(object): if warfareBuffID == 11: # Shield Burst: Active Shielding: Repair Duration/Capacitor self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill( - "Shield Emission Systems"), "capacitorNeed", value) + lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill( + "Shield Emission Systems"), "capacitorNeed", value) self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill( - "Shield Emission Systems"), "duration", value) + lambda mod: mod.item.requiresSkill("Shield Operation") or mod.item.requiresSkill( + "Shield Emission Systems"), "duration", value) if warfareBuffID == 12: # Shield Burst: Shield Extension: Shield HP self.ship.boostItemAttr("shieldCapacity", value, stackingPenalties=True) @@ -506,26 +508,26 @@ class Fit(object): if warfareBuffID == 17: # Information Burst: Electronic Superiority: EWAR Range and Strength groups = ("ECM", "Sensor Dampener", "Weapon Disruptor", "Target Painter") self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, "maxRange", value, - stackingPenalties=True) + stackingPenalties=True) self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, - "falloffEffectiveness", value, stackingPenalties=True) + "falloffEffectiveness", value, stackingPenalties=True) for scanType in ("Magnetometric", "Radar", "Ladar", "Gravimetric"): self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "ECM", - "scan%sStrengthBonus" % scanType, value, - stackingPenalties=True) + "scan%sStrengthBonus" % scanType, value, + stackingPenalties=True) for attr in ("missileVelocityBonus", "explosionDelayBonus", "aoeVelocityBonus", "falloffBonus", "maxRangeBonus", "aoeCloudSizeBonus", "trackingSpeedBonus"): self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Weapon Disruptor", - attr, value) + attr, value) for attr in ("maxTargetRangeBonus", "scanResolutionBonus"): self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Sensor Dampener", - attr, value) + attr, value) self.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Target Painter", - "signatureRadiusBonus", value, stackingPenalties=True) + "signatureRadiusBonus", value, stackingPenalties=True) if warfareBuffID == 18: # Information Burst: Electronic Hardening: Scan Strength for scanType in ("Gravimetric", "Radar", "Ladar", "Magnetometric"): @@ -543,41 +545,33 @@ class Fit(object): if warfareBuffID == 21: # Skirmish Burst: Interdiction Maneuvers: Tackle Range groups = ("Stasis Web", "Warp Scrambler") - self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, "maxRange", value, - stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.group.name in groups, "maxRange", value, stackingPenalties=True) if warfareBuffID == 22: # Skirmish Burst: Rapid Deployment: AB/MWD Speed Increase - self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Afterburner") - or mod.item.requiresSkill("High Speed Maneuvering"), - "speedFactor", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Afterburner") or mod.item.requiresSkill("High Speed Maneuvering"), + "speedFactor", value, stackingPenalties=True) if warfareBuffID == 23: # Mining Burst: Mining Laser Field Enhancement: Mining/Survey Range - self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Mining") or - mod.item.requiresSkill("Ice Harvesting") or - mod.item.requiresSkill("Gas Cloud Harvesting"), - "maxRange", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or + mod.item.requiresSkill("Ice Harvesting") or + mod.item.requiresSkill("Gas Cloud Harvesting"), + "maxRange", value, stackingPenalties=True) - self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("CPU Management"), - "surveyScanRange", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("CPU Management"), "surveyScanRange", value, stackingPenalties=True) if warfareBuffID == 24: # Mining Burst: Mining Laser Optimization: Mining Capacitor/Duration - self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Mining") or - mod.item.requiresSkill("Ice Harvesting") or - mod.item.requiresSkill("Gas Cloud Harvesting"), - "capacitorNeed", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or + mod.item.requiresSkill("Ice Harvesting") or + mod.item.requiresSkill("Gas Cloud Harvesting"), + "capacitorNeed", value, stackingPenalties=True) - self.modules.filteredItemBoost( - lambda mod: mod.item.requiresSkill("Mining") or - mod.item.requiresSkill("Ice Harvesting") or - mod.item.requiresSkill("Gas Cloud Harvesting"), - "duration", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining") or + mod.item.requiresSkill("Ice Harvesting") or + mod.item.requiresSkill("Gas Cloud Harvesting"), + "duration", value, stackingPenalties=True) if warfareBuffID == 25: # Mining Burst: Mining Equipment Preservation: Crystal Volatility - self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining"), - "crystalVolatilityChance", value, stackingPenalties=True) + self.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Mining"), "crystalVolatilityChance", value, stackingPenalties=True) if warfareBuffID == 60: # Skirmish Burst: Evasive Maneuvers: Agility self.ship.boostItemAttr("agility", value, stackingPenalties=True) @@ -850,15 +844,17 @@ class Fit(object): 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"} + 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" + } def getSlotsFree(self, type, countDummies=False): if type in (Slot.MODE, Slot.SYSTEM): @@ -1007,29 +1003,35 @@ class Fit(object): def calculateSustainableTank(self, effective=True): if self.__sustainableTank is None: if self.capStable: - sustainable = {"armorRepair": self.extraAttributes["armorRepair"], - "shieldRepair": self.extraAttributes["shieldRepair"], - "hullRepair": self.extraAttributes["hullRepair"]} + sustainable = { + "armorRepair" : self.extraAttributes["armorRepair"], + "shieldRepair": self.extraAttributes["shieldRepair"], + "hullRepair" : self.extraAttributes["hullRepair"] + } else: sustainable = {} repairers = [] # Map a repairer type to the attribute it uses - groupAttrMap = {"Armor Repair Unit": "armorDamageAmount", - "Ancillary Armor Repairer": "armorDamageAmount", - "Hull Repair Unit": "structureDamageAmount", - "Shield Booster": "shieldBonus", - "Ancillary Shield Booster": "shieldBonus", - "Remote Armor Repairer": "armorDamageAmount", - "Remote Shield Booster": "shieldBonus"} + groupAttrMap = { + "Armor Repair Unit" : "armorDamageAmount", + "Ancillary Armor Repairer": "armorDamageAmount", + "Hull Repair Unit" : "structureDamageAmount", + "Shield Booster" : "shieldBonus", + "Ancillary Shield Booster": "shieldBonus", + "Remote Armor Repairer" : "armorDamageAmount", + "Remote Shield Booster" : "shieldBonus" + } # Map repairer type to attribute - groupStoreMap = {"Armor Repair Unit": "armorRepair", - "Hull Repair Unit": "hullRepair", - "Shield Booster": "shieldRepair", - "Ancillary Shield Booster": "shieldRepair", - "Remote Armor Repairer": "armorRepair", - "Remote Shield Booster": "shieldRepair", - "Ancillary Armor Repairer": "armorRepair", } + groupStoreMap = { + "Armor Repair Unit" : "armorRepair", + "Hull Repair Unit" : "hullRepair", + "Shield Booster" : "shieldRepair", + "Ancillary Shield Booster": "shieldRepair", + "Remote Armor Repairer" : "armorRepair", + "Remote Shield Booster" : "shieldRepair", + "Ancillary Armor Repairer": "armorRepair", + } capUsed = self.capUsed for attr in ("shieldRepair", "armorRepair", "hullRepair"): @@ -1057,7 +1059,7 @@ class Fit(object): # Sort repairers by efficiency. We want to use the most efficient repairers first repairers.sort(key=lambda _mod: _mod.getModifiedItemAttr( - groupAttrMap[_mod.item.group.name]) / _mod.getModifiedItemAttr("capacitorNeed"), reverse=True) + groupAttrMap[_mod.item.group.name]) / _mod.getModifiedItemAttr("capacitorNeed"), reverse=True) # Loop through every module until we're above peak recharge # Most efficient first, as we sorted earlier. @@ -1370,10 +1372,10 @@ class Fit(object): def __repr__(self): return u"Fit(ID={}, ship={}, name={}) at {}".format( - self.ID, self.ship.item.name, self.name, hex(id(self)) + self.ID, self.ship.item.name, self.name, hex(id(self)) ).encode('utf8') def __str__(self): return u"{} ({})".format( - self.name, self.ship.item.name + self.name, self.ship.item.name ).encode('utf8') diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index bac688769..8287f349a 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -173,7 +173,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): charges = 0 else: charges = floor(containerCapacity / chargeVolume) - return charges + return int(charges) @property def numShots(self): diff --git a/gui/builtinContextMenus/__init__.py b/gui/builtinContextMenus/__init__.py index bb8631623..e69de29bb 100644 --- a/gui/builtinContextMenus/__init__.py +++ b/gui/builtinContextMenus/__init__.py @@ -1,27 +0,0 @@ -__all__ = [ - "openFit", - # "moduleGlobalAmmoPicker", - "moduleAmmoPicker", - "itemStats", - "damagePattern", - "marketJump", - "droneSplit", - "itemRemove", - "droneRemoveStack", - "ammoPattern", - "project", - "factorReload", - "whProjector", - "cargo", - "shipJump", - "changeAffectingSkills", - "tacticalMode", - "targetResists", - "priceClear", - "amount", - "metaSwap", - "implantSets", - "fighterAbilities", - "cargoAmmo", - "droneStack" -] diff --git a/gui/builtinContextMenus/cargoAmmo.py b/gui/builtinContextMenus/cargoAmmo.py index 573e4c24f..12ebe552f 100644 --- a/gui/builtinContextMenus/cargoAmmo.py +++ b/gui/builtinContextMenus/cargoAmmo.py @@ -1,15 +1,20 @@ from gui.contextMenu import ContextMenu import gui.mainFrame -import service import gui.globalEvents as GE import wx +from service.settings import ContextMenuSettings +from service.fit import Fit class CargoAmmo(ContextMenu): def __init__(self): self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.settings = ContextMenuSettings.getInstance() def display(self, srcContext, selection): + if not self.settings.get('cargoAmmo'): + return False + if srcContext not in ("marketItemGroup", "marketItemMisc") or self.mainFrame.getActiveFit() is None: return False @@ -23,7 +28,7 @@ class CargoAmmo(ContextMenu): return "Add {0} to Cargo (x1000)".format(itmContext) def activate(self, fullContext, selection, i): - sFit = service.Fit.getInstance() + sFit = Fit.getInstance() fitID = self.mainFrame.getActiveFit() typeID = int(selection[0].ID) diff --git a/gui/builtinContextMenus/droneStack.py b/gui/builtinContextMenus/droneStack.py index b7fde1040..99a253796 100644 --- a/gui/builtinContextMenus/droneStack.py +++ b/gui/builtinContextMenus/droneStack.py @@ -1,15 +1,20 @@ from gui.contextMenu import ContextMenu import gui.mainFrame -import service import gui.globalEvents as GE import wx +from service.settings import ContextMenuSettings +from service.fit import Fit -class CargoAmmo(ContextMenu): +class DroneStack(ContextMenu): def __init__(self): self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.settings = ContextMenuSettings.getInstance() def display(self, srcContext, selection): + if not self.settings.get('droneStack'): + return False + if srcContext not in ("marketItemGroup", "marketItemMisc") or self.mainFrame.getActiveFit() is None: return False @@ -25,7 +30,7 @@ class CargoAmmo(ContextMenu): return "Add {0} to Drone Bay (x5)".format(itmContext) def activate(self, fullContext, selection, i): - sFit = service.Fit.getInstance() + sFit = Fit.getInstance() fitID = self.mainFrame.getActiveFit() typeID = int(selection[0].ID) @@ -34,4 +39,4 @@ class CargoAmmo(ContextMenu): wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) -CargoAmmo.register() +DroneStack.register() diff --git a/gui/builtinStatsViews/capacitorViewFull.py b/gui/builtinStatsViews/capacitorViewFull.py index f85b35e4c..49bb9d59b 100644 --- a/gui/builtinStatsViews/capacitorViewFull.py +++ b/gui/builtinStatsViews/capacitorViewFull.py @@ -114,6 +114,10 @@ class CapacitorViewFull(StatsView): ("label%sCapacitorRecharge", lambda: fit.capRecharge, 3, 0, 0), ("label%sCapacitorDischarge", lambda: fit.capUsed, 3, 0, 0), ) + if fit: + neut_resist = fit.ship.getModifiedItemAttr("energyWarfareResistance", 0) + else: + neut_resist = 0 panel = "Full" for labelName, value, prec, lowest, highest in stats: @@ -127,6 +131,12 @@ class CapacitorViewFull(StatsView): label.SetLabel(formatAmount(value, prec, lowest, highest)) label.SetToolTip(wx.ToolTip("%.1f" % value)) + if labelName == "label%sCapacitorDischarge": + if neut_resist: + neut_resist = 100 - (neut_resist * 100) + label_tooltip = "Neut Resistance: {0:.0f}%".format(neut_resist) + label.SetToolTip(wx.ToolTip(label_tooltip)) + capState = fit.capState if fit is not None else 0 capStable = fit.capStable if fit is not None else False lblNameTime = "label%sCapacitorTime" diff --git a/gui/builtinStatsViews/outgoingViewMinimal.py b/gui/builtinStatsViews/outgoingViewMinimal.py index 70f7994dd..d2bf2c118 100644 --- a/gui/builtinStatsViews/outgoingViewMinimal.py +++ b/gui/builtinStatsViews/outgoingViewMinimal.py @@ -20,7 +20,6 @@ # noinspection PyPackageRequirements import wx from gui.statsView import StatsView -from gui.bitmapLoader import BitmapLoader from gui.utils.numberFormatter import formatAmount diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index 7f5c6cb14..496cf4fb8 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -150,7 +150,6 @@ class FittingView(d.Display): self.activeFitID = None self.FVsnapshot = None self.itemCount = 0 - self.itemRect = 0 self.hoveredRow = None self.hoveredColumn = None @@ -269,9 +268,7 @@ class FittingView(d.Display): We also refresh the fit of the new current page in case delete fit caused change in stats (projected) """ - fitID = event.fitID - - if fitID == self.getActiveFit(): + if event.fitID == self.getActiveFit(): self.parent.DeletePage(self.parent.GetPageIndex(self)) try: @@ -280,7 +277,7 @@ class FittingView(d.Display): sFit.refreshFit(self.getActiveFit()) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID)) except wx._core.PyDeadObjectError: - pyfalog.warning("Caught dead object") + pyfalog.error("Caught dead object") pass event.Skip() @@ -486,7 +483,7 @@ class FittingView(d.Display): self.Show(self.activeFitID is not None and self.activeFitID == event.fitID) except wx._core.PyDeadObjectError: - pyfalog.warning("Caught dead object") + pyfalog.error("Caught dead object") finally: event.Skip() @@ -634,7 +631,6 @@ class FittingView(d.Display): self.Thaw() self.itemCount = self.GetItemCount() - self.itemRect = self.GetItemRect(0) if 'wxMac' in wx.PlatformInfo: try: diff --git a/gui/contextMenu.py b/gui/contextMenu.py index 243a4a743..a35c73282 100644 --- a/gui/contextMenu.py +++ b/gui/contextMenu.py @@ -181,7 +181,7 @@ class ContextMenu(object): # noinspection PyUnresolvedReferences from gui.builtinContextMenus import ( # noqa: E402,F401 openFit, - # moduleGlobalAmmoPicker, + moduleGlobalAmmoPicker, moduleAmmoPicker, itemStats, damagePattern, @@ -200,6 +200,8 @@ from gui.builtinContextMenus import ( # noqa: E402,F401 targetResists, priceClear, amount, + cargoAmmo, + droneStack, metaSwap, implantSets, fighterAbilities, diff --git a/gui/graphFrame.py b/gui/graphFrame.py index 7a0aa3387..2abeadbab 100644 --- a/gui/graphFrame.py +++ b/gui/graphFrame.py @@ -29,13 +29,14 @@ import gui.mainFrame import gui.globalEvents as GE from gui.graph import Graph from gui.bitmapLoader import BitmapLoader +import traceback pyfalog = Logger(__name__) try: import matplotlib as mpl - mpl_version = int(mpl.__version__[0]) + mpl_version = int(mpl.__version__[0]) or -1 if mpl_version >= 2: mpl.use('wxagg') mplImported = True @@ -48,43 +49,33 @@ try: graphFrame_enabled = True mplImported = True -except ImportError: +except ImportError as e: + pyfalog.warning("Matplotlib failed to import. Likely missing or incompatible version.") + mpl_version = -1 + Patch = mpl = Canvas = Figure = None + graphFrame_enabled = False + mplImported = False +except Exception: + # We can get exceptions deep within matplotlib. Catch those. See GH #1046 + tb = traceback.format_exc() + pyfalog.critical("Exception when importing Matplotlib. Continuing without importing.") + pyfalog.critical(tb) + mpl_version = -1 Patch = mpl = Canvas = Figure = None graphFrame_enabled = False mplImported = False -pyfalog = Logger(__name__) - class GraphFrame(wx.Frame): def __init__(self, parent, style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE | wx.FRAME_FLOAT_ON_PARENT): - global graphFrame_enabled global mplImported - - self.Patch = None - self.mpl_version = -1 - - try: - import matplotlib as mpl - self.mpl_version = int(mpl.__version__[0]) - if self.mpl_version >= 2: - mpl.use('wxagg') - mplImported = True - else: - mplImported = False - from matplotlib.patches import Patch - self.Patch = Patch - from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas - from matplotlib.figure import Figure - graphFrame_enabled = True - except ImportError: - Patch = mpl = Canvas = Figure = None - graphFrame_enabled = False + global mpl_version self.legendFix = False + if not graphFrame_enabled: - pyfalog.info("Problems importing matplotlib; continuing without graphs") + pyfalog.warning("Matplotlib is not enabled. Skipping initialization.") return try: @@ -236,6 +227,8 @@ class GraphFrame(wx.Frame): self.draw() def draw(self, event=None): + global mpl_version + values = self.getValues() view = self.getView() self.subplot.clear() @@ -260,7 +253,7 @@ class GraphFrame(wx.Frame): self.canvas.draw() return - if self.mpl_version < 2: + if mpl_version < 2: if self.legendFix and len(legend) > 0: leg = self.subplot.legend(tuple(legend), "upper right", shadow=False) for t in leg.get_texts(): @@ -276,7 +269,7 @@ class GraphFrame(wx.Frame): for l in leg.get_lines(): l.set_linewidth(1) - elif self.mpl_version >= 2: + elif mpl_version >= 2: legend2 = [] legend_colors = { 0: "blue", diff --git a/gui/itemStats.py b/gui/itemStats.py index 325793e86..3ac94ce0c 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -1043,7 +1043,7 @@ class ItemAffectedBy(wx.Panel): container = {} for attrName in attributes.iterAfflictions(): # if value is 0 or there has been no change from original to modified, return - if attributes[attrName] == (attributes.getOriginal(attrName) or 0): + if attributes[attrName] == (attributes.getOriginal(attrName, 0)): continue for fit, afflictors in attributes.getAfflictions(attrName).iteritems(): @@ -1170,7 +1170,7 @@ class ItemAffectedBy(wx.Panel): container = {} for attrName in attributes.iterAfflictions(): # if value is 0 or there has been no change from original to modified, return - if attributes[attrName] == (attributes.getOriginal(attrName) or 0): + if attributes[attrName] == (attributes.getOriginal(attrName, 0)): continue for fit, afflictors in attributes.getAfflictions(attrName).iteritems(): diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 7af37d875..8a66e78d2 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -651,11 +651,11 @@ class MainFrame(wx.Frame): dlg.Show() def toggleOverrides(self, event): - ModifiedAttributeDict.OVERRIDES = not ModifiedAttributeDict.OVERRIDES + ModifiedAttributeDict.overrides_enabled = not ModifiedAttributeDict.overrides_enabled wx.PostEvent(self, GE.FitChanged(fitID=self.getActiveFit())) menu = self.GetMenuBar() menu.SetLabel(menu.toggleOverridesId, - "Turn Overrides Off" if ModifiedAttributeDict.OVERRIDES else "Turn Overrides On") + "Turn Overrides Off" if ModifiedAttributeDict.overrides_enabled else "Turn Overrides On") def saveChar(self, event): sChr = Character.getInstance() diff --git a/gui/shipBrowser.py b/gui/shipBrowser.py index 33561de5b..c61f9b698 100644 --- a/gui/shipBrowser.py +++ b/gui/shipBrowser.py @@ -1746,6 +1746,7 @@ class FitItem(SFItem.SFBrowserItem): self.deleteFit() def deleteFit(self, event=None): + pyfalog.debug("Deleting ship fit.") if self.deleted: return else: diff --git a/pyfa.py b/pyfa.py index 488165724..06f8632d1 100755 --- a/pyfa.py +++ b/pyfa.py @@ -26,6 +26,7 @@ import config from optparse import OptionParser, BadOptionError, AmbiguousOptionError +import logbook from logbook import TimedRotatingFileHandler, Logger, StreamHandler, NestedSetup, FingersCrossedHandler, NullHandler, \ CRITICAL, ERROR, WARNING, DEBUG, INFO pyfalog = Logger(__name__) @@ -144,6 +145,10 @@ if not hasattr(sys, 'frozen'): print("Cannot find python-dateutil.\nYou can download python-dateutil from https://pypi.python.org/pypi/python-dateutil") sys.exit(1) + logVersion = logbook.__version__.split('.') + if int(logVersion[0]) < 1: + print ("Logbook version >= 1.0.0 is recommended. You may have some performance issues by continuing to use an earlier version.") + if __name__ == "__main__": # Configure paths diff --git a/requirements.txt b/requirements.txt index ed301fe25..d3bc33dde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -logbook +logbook>=1.0.0 matplotlib PyYAML python-dateutil diff --git a/service/fit.py b/service/fit.py index 70a471042..a5889f3ec 100644 --- a/service/fit.py +++ b/service/fit.py @@ -19,6 +19,7 @@ import copy from logbook import Logger +from time import time import eos.db from eos.saveddata.booster import Booster as es_Booster @@ -79,12 +80,14 @@ class Fit(object): @staticmethod def getAllFits(): + pyfalog.debug("Fetching all fits") fits = eos.db.getFitList() return fits @staticmethod def getFitsWithShip(shipID): """ Lists fits of shipID, used with shipBrowser """ + pyfalog.debug("Fetching all fits for ship ID: {0}", shipID) fits = eos.db.getFitsWithShip(shipID) names = [] for fit in fits: @@ -95,6 +98,7 @@ class Fit(object): @staticmethod def getBoosterFits(): """ Lists fits flagged as booster """ + pyfalog.debug("Fetching all fits flagged as a booster.") fits = eos.db.getBoosterFits() names = [] for fit in fits: @@ -104,10 +108,12 @@ class Fit(object): @staticmethod def countAllFits(): + pyfalog.debug("Getting count of all fits.") return eos.db.countAllFits() @staticmethod def countFitsWithShip(stuff): + pyfalog.debug("Getting count of all fits for: {0}", stuff) count = eos.db.countFitsWithShip(stuff) return count @@ -117,6 +123,7 @@ class Fit(object): return fit.modules[pos] def newFit(self, shipID, name=None): + pyfalog.debug("Creating new fit for ID: {0}", shipID) try: ship = es_Ship(eos.db.getItem(shipID)) except ValueError: @@ -133,18 +140,21 @@ class Fit(object): @staticmethod def toggleBoostFit(fitID): + pyfalog.debug("Toggling as booster for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) fit.booster = not fit.booster eos.db.commit() @staticmethod def renameFit(fitID, newName): + pyfalog.debug("Renaming fit ({0}) to: {1}", fitID, newName) fit = eos.db.getFit(fitID) fit.name = newName eos.db.commit() @staticmethod def deleteFit(fitID): + pyfalog.debug("Deleting fit for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) eos.db.remove(fit) @@ -157,6 +167,7 @@ class Fit(object): @staticmethod def copyFit(fitID): + pyfalog.debug("Creating copy of fit ID: {0}", fitID) fit = eos.db.getFit(fitID) newFit = copy.deepcopy(fit) eos.db.save(newFit) @@ -164,6 +175,7 @@ class Fit(object): @staticmethod def clearFit(fitID): + pyfalog.debug("Clearing fit for fit ID: {0}", fitID) if fitID is None: return None @@ -172,6 +184,7 @@ class Fit(object): return fit def toggleFactorReload(self, fitID): + pyfalog.debug("Toggling factor reload for fit ID: {0}", fitID) if fitID is None: return None @@ -181,6 +194,7 @@ class Fit(object): self.recalc(fit) def switchFit(self, fitID): + pyfalog.debug("Switching fit to fit ID: {0}", fitID) if fitID is None: return None @@ -204,6 +218,7 @@ class Fit(object): Projected is a recursion flag that is set to reduce recursions into projected fits Basic is a flag to simply return the fit without any other processing """ + pyfalog.debug("Getting fit for fit ID: {0}", fitID) if fitID is None: return None fit = eos.db.getFit(fitID) @@ -229,6 +244,7 @@ class Fit(object): @staticmethod def searchFits(name): + pyfalog.debug("Searching for fit: {0}", name) results = eos.db.searchFits(name) fits = [] for fit in results: @@ -238,6 +254,7 @@ class Fit(object): return fits def addImplant(self, fitID, itemID, recalc=True): + pyfalog.debug("Adding implant to fit ({0}) for item ID: {1}", fitID, itemID) if fitID is None: return False @@ -255,6 +272,7 @@ class Fit(object): return True def removeImplant(self, fitID, position): + pyfalog.debug("Removing implant from position ({0}) for fit ID: {1}", position, fitID) if fitID is None: return False @@ -265,6 +283,7 @@ class Fit(object): return True def addBooster(self, fitID, itemID): + pyfalog.debug("Adding booster ({0}) to fit ID: {1}", itemID, fitID) if fitID is None: return False @@ -281,6 +300,7 @@ class Fit(object): return True def removeBooster(self, fitID, position): + pyfalog.debug("Removing booster from position ({0}) for fit ID: {1}", position, fitID) if fitID is None: return False @@ -291,6 +311,7 @@ class Fit(object): return True def project(self, fitID, thing): + pyfalog.debug("Projecting fit ({0}) onto: {1}", fitID, thing) if fitID is None: return @@ -340,6 +361,7 @@ class Fit(object): return True def addCommandFit(self, fitID, thing): + pyfalog.debug("Projecting command fit ({0}) onto: {1}", fitID, thing) if fitID is None: return @@ -359,6 +381,7 @@ class Fit(object): return True def toggleProjected(self, fitID, thing, click): + pyfalog.debug("Toggling projected on fit ({0}) for: {1}", fitID, thing) fit = eos.db.getFit(fitID) if isinstance(thing, es_Drone): if thing.amountActive == 0 and thing.canBeApplied(fit): @@ -380,6 +403,7 @@ class Fit(object): self.recalc(fit) def toggleCommandFit(self, fitID, thing): + pyfalog.debug("Toggle command fit ({0}) for: {1}", fitID, thing) fit = eos.db.getFit(fitID) commandInfo = thing.getCommandInfo(fitID) if commandInfo: @@ -390,6 +414,7 @@ class Fit(object): def changeAmount(self, fitID, projected_fit, amount): """Change amount of projected fits""" + pyfalog.debug("Changing fit ({0}) for projected fit ({1}) to new amount: {2}", fitID, projected_fit.getProjectionInfo(fitID), amount) fit = eos.db.getFit(fitID) amount = min(20, max(1, amount)) # 1 <= a <= 20 projectionInfo = projected_fit.getProjectionInfo(fitID) @@ -400,6 +425,7 @@ class Fit(object): self.recalc(fit) def changeActiveFighters(self, fitID, fighter, amount): + pyfalog.debug("Changing active fighters ({0}) for fit ({1}) to amount: {2}", fighter.itemID, amount) fit = eos.db.getFit(fitID) fighter.amountActive = amount @@ -407,6 +433,7 @@ class Fit(object): self.recalc(fit) def removeProjected(self, fitID, thing): + pyfalog.debug("Removing projection on fit ({0}) from: {1}", fitID, thing) fit = eos.db.getFit(fitID) if isinstance(thing, es_Drone): fit.projectedDrones.remove(thing) @@ -422,6 +449,7 @@ class Fit(object): self.recalc(fit) def removeCommand(self, fitID, thing): + pyfalog.debug("Removing command projection from fit ({0}) for: {1}", fitID, thing) fit = eos.db.getFit(fitID) del fit.__commandFits[thing.ID] @@ -429,6 +457,7 @@ class Fit(object): self.recalc(fit) def appendModule(self, fitID, itemID): + pyfalog.debug("Appending module for fit ({0}) using item: {1}", fitID, itemID) fit = eos.db.getFit(fitID) item = eos.db.getItem(itemID, eager=("attributes", "group.category")) try: @@ -460,6 +489,7 @@ class Fit(object): return None def removeModule(self, fitID, position): + pyfalog.debug("Removing module from position ({0}) for fit ID: {1}", position, fitID) fit = eos.db.getFit(fitID) if fit.modules[position].isEmpty: return None @@ -473,6 +503,7 @@ class Fit(object): return numSlots != len(fit.modules) def changeModule(self, fitID, position, newItemID): + pyfalog.debug("Changing position of module from position ({0}) for fit ID: {1}", position, fitID) fit = eos.db.getFit(fitID) # Dummy it out in case the next bit fails @@ -511,6 +542,7 @@ class Fit(object): sanity checks as opposed to the GUI View. This is different than how the normal .swapModules() does things, which is mostly a blind swap. """ + pyfalog.debug("Moving cargo item to module for fit ID: {1}", fitID) fit = eos.db.getFit(fitID) module = fit.modules[moduleIdx] @@ -558,6 +590,7 @@ class Fit(object): @staticmethod def swapModules(fitID, src, dst): + pyfalog.debug("Swapping modules from source ({0}) to destination ({1}) for fit ID: {1}", src, dst, fitID) fit = eos.db.getFit(fitID) # Gather modules srcMod = fit.modules[src] @@ -577,6 +610,7 @@ class Fit(object): This will overwrite dst! Checking for empty module must be done at a higher level """ + pyfalog.debug("Cloning modules from source ({0}) to destination ({1}) for fit ID: {1}", src, dst, fitID) fit = eos.db.getFit(fitID) # Gather modules srcMod = fit.modules[src] @@ -597,6 +631,7 @@ class Fit(object): Adds cargo via typeID of item. If replace = True, we replace amount with given parameter, otherwise we increment """ + pyfalog.debug("Adding cargo ({0}) fit ID: {1}", itemID, fitID) if fitID is None: return False @@ -629,6 +664,7 @@ class Fit(object): return True def removeCargo(self, fitID, position): + pyfalog.debug("Removing cargo from position ({0}) fit ID: {1}", position, fitID) if fitID is None: return False @@ -639,6 +675,7 @@ class Fit(object): return True def addFighter(self, fitID, itemID): + pyfalog.debug("Adding fighters ({0}) to fit ID: {1}", itemID, fitID) if fitID is None: return False @@ -685,6 +722,7 @@ class Fit(object): return False def removeFighter(self, fitID, i): + pyfalog.debug("Removing fighters from fit ID: {0}", fitID) fit = eos.db.getFit(fitID) f = fit.fighters[i] fit.fighters.remove(f) @@ -694,6 +732,7 @@ class Fit(object): return True def addDrone(self, fitID, itemID, numDronesToAdd=1): + pyfalog.debug("Adding {0} drones ({1}) to fit ID: {2}", numDronesToAdd, itemID, fitID) if fitID is None: return False @@ -720,6 +759,7 @@ class Fit(object): return False def mergeDrones(self, fitID, d1, d2, projected=False): + pyfalog.debug("Merging drones on fit ID: {0}", fitID) if fitID is None: return False @@ -746,6 +786,7 @@ class Fit(object): @staticmethod def splitDrones(fit, d, amount, l): + pyfalog.debug("Splitting drones for fit ID: {0}", fit) total = d.amount active = d.amountActive > 0 d.amount = amount @@ -758,6 +799,7 @@ class Fit(object): eos.db.commit() def splitProjectedDroneStack(self, fitID, d, amount): + pyfalog.debug("Splitting projected drone stack for fit ID: {0}", fitID) if fitID is None: return False @@ -765,6 +807,7 @@ class Fit(object): self.splitDrones(fit, d, amount, fit.projectedDrones) def splitDroneStack(self, fitID, d, amount): + pyfalog.debug("Splitting drone stack for fit ID: {0}", fitID) if fitID is None: return False @@ -772,6 +815,7 @@ class Fit(object): self.splitDrones(fit, d, amount, fit.drones) def removeDrone(self, fitID, i, numDronesToRemove=1): + pyfalog.debug("Removing {0} drones for fit ID: {1}", numDronesToRemove, fitID) fit = eos.db.getFit(fitID) d = fit.drones[i] d.amount -= numDronesToRemove @@ -786,6 +830,7 @@ class Fit(object): return True def toggleDrone(self, fitID, i): + pyfalog.debug("Toggling drones for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) d = fit.drones[i] if d.amount == d.amountActive: @@ -798,6 +843,7 @@ class Fit(object): return True def toggleFighter(self, fitID, i): + pyfalog.debug("Toggling fighters for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) f = fit.fighters[i] f.active = not f.active @@ -807,6 +853,7 @@ class Fit(object): return True def toggleImplant(self, fitID, i): + pyfalog.debug("Toggling implant for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) implant = fit.implants[i] implant.active = not implant.active @@ -816,6 +863,7 @@ class Fit(object): return True def toggleImplantSource(self, fitID, source): + pyfalog.debug("Toggling implant source for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) fit.implantSource = source @@ -824,6 +872,7 @@ class Fit(object): return True def toggleBooster(self, fitID, i): + pyfalog.debug("Toggling booster for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) booster = fit.boosters[i] booster.active = not booster.active @@ -833,12 +882,14 @@ class Fit(object): return True def toggleFighterAbility(self, fitID, ability): + pyfalog.debug("Toggling fighter ability for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) ability.active = not ability.active eos.db.commit() self.recalc(fit) def changeChar(self, fitID, charID): + pyfalog.debug("Changing character ({0}) for fit ID: {1}", charID, fitID) if fitID is None or charID is None: if charID is not None: self.character = Character.getInstance().all5() @@ -854,6 +905,7 @@ class Fit(object): return eos.db.getItem(itemID).category.name == "Charge" def setAmmo(self, fitID, ammoID, modules): + pyfalog.debug("Set ammo for fit ID: {0}", fitID) if fitID is None: return @@ -868,6 +920,7 @@ class Fit(object): @staticmethod def getTargetResists(fitID): + pyfalog.debug("Get target resists for fit ID: {0}", fitID) if fitID is None: return @@ -875,6 +928,7 @@ class Fit(object): return fit.targetResists def setTargetResists(self, fitID, pattern): + pyfalog.debug("Set target resist for fit ID: {0}", fitID) if fitID is None: return @@ -886,6 +940,7 @@ class Fit(object): @staticmethod def getDamagePattern(fitID): + pyfalog.debug("Get damage pattern for fit ID: {0}", fitID) if fitID is None: return @@ -893,6 +948,7 @@ class Fit(object): return fit.damagePattern def setDamagePattern(self, fitID, pattern): + pyfalog.debug("Set damage pattern for fit ID: {0}", fitID) if fitID is None: return @@ -903,6 +959,7 @@ class Fit(object): self.recalc(fit) def setMode(self, fitID, mode): + pyfalog.debug("Set mode for fit ID: {0}", fitID) if fitID is None: return @@ -913,6 +970,7 @@ class Fit(object): self.recalc(fit) def setAsPattern(self, fitID, ammo): + pyfalog.debug("Set as pattern for fit ID: {0}", fitID) if fitID is None: return @@ -930,6 +988,7 @@ class Fit(object): self.recalc(fit) def checkStates(self, fit, base): + pyfalog.debug("Check states for fit ID: {0}", fit) changed = False for mod in fit.modules: if mod != base: @@ -954,6 +1013,7 @@ class Fit(object): self.recalc(fit) def toggleModulesState(self, fitID, base, modules, click): + pyfalog.debug("Toggle module state for fit ID: {0}", fitID) changed = False proposedState = self.__getProposedState(base, click) @@ -993,6 +1053,7 @@ class Fit(object): State.ONLINE: State.OFFLINE} def __getProposedState(self, mod, click, proposedState=None): + pyfalog.debug("Get proposed state for module.") if mod.slot == Slot.SUBSYSTEM or mod.isEmpty: return State.ONLINE @@ -1020,6 +1081,7 @@ class Fit(object): return currState def refreshFit(self, fitID): + pyfalog.debug("Refresh fit for fit ID: {0}", fitID) if fitID is None: return None @@ -1028,9 +1090,12 @@ class Fit(object): self.recalc(fit) def recalc(self, fit, withBoosters=True): + start_time = time() pyfalog.info("=" * 10 + "recalc" + "=" * 10) if fit.factorReload is not self.serviceFittingOptions["useGlobalForceReload"]: fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"] fit.clear() fit.calculateModifiedAttributes(withBoosters=False) + + pyfalog.info("=" * 10 + "recalc time: " + str(time() - start_time) + "=" * 10) diff --git a/service/market.py b/service/market.py index e311c94e5..70dcd1e71 100644 --- a/service/market.py +++ b/service/market.py @@ -229,6 +229,7 @@ class Market(object): "Apotheosis" : self.les_grp, # 5th EVE anniversary present "Zephyr" : self.les_grp, # 2010 new year gift "Primae" : self.les_grp, # Promotion of planetary interaction + "Council Diplomatic Shuttle" : self.les_grp, # CSM X celebration "Freki" : self.les_grp, # AT7 prize "Mimir" : self.les_grp, # AT7 prize "Utu" : self.les_grp, # AT8 prize @@ -274,7 +275,6 @@ class Market(object): "Guristas Shuttle" : False, "Mobile Decoy Unit" : False, # Seems to be left over test mod for deployables "Tournament Micro Jump Unit" : False, # Normally seen only on tournament arenas - "Council Diplomatic Shuttle" : False, # CSM X celebration "Civilian Gatling Railgun" : True, "Civilian Gatling Pulse Laser" : True, "Civilian Gatling Autocannon" : True, diff --git a/service/settings.py b/service/settings.py index 5d44d4e88..69dc8130d 100644 --- a/service/settings.py +++ b/service/settings.py @@ -29,7 +29,8 @@ pyfalog = Logger(__name__) class SettingsProvider(object): - BASE_PATH = os.path.join(config.savePath, 'settings') + if config.savePath: + BASE_PATH = os.path.join(config.savePath, 'settings') settings = {} _instance = None @@ -41,13 +42,15 @@ class SettingsProvider(object): return cls._instance def __init__(self): - if not os.path.exists(self.BASE_PATH): - os.mkdir(self.BASE_PATH) + if hasattr(self, 'BASE_PATH'): + if not os.path.exists(self.BASE_PATH): + os.mkdir(self.BASE_PATH) def getSettings(self, area, defaults=None): s = self.settings.get(area) - if s is None: + + if s is None and hasattr(self, 'BASE_PATH'): p = os.path.join(self.BASE_PATH, area) if not os.path.exists(p): @@ -71,6 +74,8 @@ class SettingsProvider(object): info[item] = defaults[item] self.settings[area] = s = Settings(p, info) + else: + s = None return s @@ -408,14 +413,17 @@ class ContextMenuSettings(object): "ammoPattern" : 1, "amount" : 1, "cargo" : 1, + "cargoAmmo" : 1, "changeAffectingSkills" : 1, "damagePattern" : 1, "droneRemoveStack" : 1, "droneSplit" : 1, + "droneStack" : 1, "factorReload" : 1, "fighterAbilities" : 1, - "implantSet" : 1, + "implantSets" : 1, "itemStats" : 1, + "itemRemove" : 1, "marketJump" : 1, "metaSwap" : 1, "moduleAmmoPicker" : 1, diff --git a/tests/test_modules/gui/test_aboutData.py b/tests/test_modules/gui/test_aboutData.py deleted file mode 100644 index 8e7e862d6..000000000 --- a/tests/test_modules/gui/test_aboutData.py +++ /dev/null @@ -1,9 +0,0 @@ -from gui.aboutData import versionString, licenses, developers, credits, description - - -def test_aboutData(): - assert versionString.__len__() > 0 - assert licenses.__len__() > 0 - assert developers.__len__() > 0 - assert credits.__len__() > 0 - assert description.__len__() > 0 diff --git a/tests/test_modules/test_eos/test_gamedata.py b/tests/test_modules/test_eos/test_gamedata.py new file mode 100644 index 000000000..4a8674339 --- /dev/null +++ b/tests/test_modules/test_eos/test_gamedata.py @@ -0,0 +1,17 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +from _development.helpers_fits import RifterFit, KeepstarFit + +def test_race(DB, RifterFit, KeepstarFit): + """ + Test race code + """ + assert RifterFit.ship.item.race == 'minmatar' + assert KeepstarFit.ship.item.race == 'upwell' diff --git a/tests/test_modules/test_eos/test_modifiedAttributeDict.py b/tests/test_modules/test_eos/test_modifiedAttributeDict.py new file mode 100644 index 000000000..55b68fb14 --- /dev/null +++ b/tests/test_modules/test_eos/test_modifiedAttributeDict.py @@ -0,0 +1,50 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import math +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +script_dir = os.path.realpath(os.path.join(script_dir, '..', '..', '..')) +print script_dir +sys.path.append(script_dir) + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +from _development.helpers_fits import RifterFit + +def test_multiply_stacking_penalties(DB, Saveddata, RifterFit): + """ + Tests the stacking penalties under multiply + """ + char0 = Saveddata['Character'].getAll0() + + RifterFit.character = char0 + starting_em_resist = RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") + + mod = Saveddata['Module'](DB['db'].getItem("EM Ward Amplifier II")) + item_modifer = mod.item.getAttribute("emDamageResistanceBonus") + + RifterFit.calculateModifiedAttributes() + + for _ in range(10): + if _ == 0: + # First run we have no modules, se don't try and calculate them. + calculated_resist = RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") + else: + # Calculate what our next resist should be + # Denominator: [math.exp((i / 2.67) ** 2.0) for i in xrange(8)] + current_effectiveness = 1 / math.exp(((_ - 1) / 2.67) ** 2.0) + new_item_modifier = 1 + ((item_modifer * current_effectiveness) / 100) + calculated_resist = (em_resist * new_item_modifier) + + # Add another resist module to our fit. + RifterFit.modules.append(mod) + + # Modify our fit so that Eos generates new numbers for us. + RifterFit.clear() + RifterFit.calculateModifiedAttributes() + + em_resist = RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") + + assert em_resist == calculated_resist + # print(str(em_resist) + "==" + str(calculated_resist)) diff --git a/tests/test_modules/test_gui/test_aboutData.py b/tests/test_modules/test_gui/test_aboutData.py new file mode 100644 index 000000000..b17668e68 --- /dev/null +++ b/tests/test_modules/test_gui/test_aboutData.py @@ -0,0 +1,19 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + +from gui.aboutData import versionString, licenses, developers, credits, description + + +def test_aboutData(): + """ + Simple test to validate all about data exists + """ + assert versionString.__len__() > 0 + assert licenses.__len__() > 0 + assert developers.__len__() > 0 + assert credits.__len__() > 0 + assert description.__len__() > 0 diff --git a/tests/test_modules/service/test_attribute.py b/tests/test_modules/test_service/test_attribute.py similarity index 82% rename from tests/test_modules/service/test_attribute.py rename to tests/test_modules/test_service/test_attribute.py index 1ab44f2a8..0f9622885 100644 --- a/tests/test_modules/service/test_attribute.py +++ b/tests/test_modules/test_service/test_attribute.py @@ -1,9 +1,16 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + from service.attribute import Attribute def test_attribute(): """ - We don't really have much to test here, to throw a generic attribute at it and validate we get the expected results + We don't really have much to test here, so throw a generic attribute at it and validate we get the expected results :return: """ diff --git a/tests/test_modules/test_service/test_fit.py b/tests/test_modules/test_service/test_fit.py new file mode 100644 index 000000000..dff83eb49 --- /dev/null +++ b/tests/test_modules/test_service/test_fit.py @@ -0,0 +1,37 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +# import os +# import sys +# script_dir = os.path.dirname(os.path.abspath(__file__)) +# sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) +# +# # noinspection PyPackageRequirements +# from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +# # noinspection PyPackageRequirements +# from _development.helpers_fits import RifterFit, KeepstarFit +# from service.fit import Fit +# +# # Fake import wx +# # todo: fix this +# # from types import ModuleType +# # wx = ModuleType("fake_module") +# # sys.modules[wx.__name__] = wx +# +# def test_getAllFits(DB, RifterFit, KeepstarFit): +# assert len(Fit.getAllFits()) == 0 +# DB['db'].save(RifterFit) +# assert len(Fit.getAllFits()) == 1 +# DB['db'].save(KeepstarFit) +# assert len(Fit.getAllFits()) == 2 +# +# # Cleanup after ourselves +# DB['db'].remove(RifterFit) +# DB['db'].remove(KeepstarFit) +# +# +# def test_getFitsWithShip_RifterFit(DB, RifterFit): +# DB['db'].save(RifterFit) +# +# assert Fit.getFitsWithShip(587)[0][1] == 'My Rifter Fit' +# +# DB['db'].remove(RifterFit) diff --git a/tests/test_package.py b/tests/test_package.py deleted file mode 100644 index 5f97967d7..000000000 --- a/tests/test_package.py +++ /dev/null @@ -1,58 +0,0 @@ -"""import tests.""" - -import os -import sys -# import importlib - -# noinspection PyPackageRequirements -# import pytest - - -script_dir = os.path.dirname(os.path.abspath(__file__)) -# Add root to python paths, this allows us to import submodules -sys.path.append(os.path.realpath(os.path.join(script_dir, '..'))) - -# noinspection PyPep8 -import service -# noinspection PyPep8 -import gui -# noinspection PyPep8 -import eos -# noinspection PyPep8 -import utils - - -def test_packages(): - assert service - assert gui - assert eos - assert utils - - -def service_modules(): - for root, folders, files in os.walk("service"): - for file_ in files: - if file_.endswith(".py") and not file_.startswith("_"): - mod_name = "{}.{}".format( - root.replace("/", "."), - file_.split(".py")[0], - ) - yield mod_name - - -def eos_modules(): - for root, folders, files in os.walk("eos"): - for file_ in files: - if file_.endswith(".py") and not file_.startswith("_"): - mod_name = "{}.{}".format( - root.replace("/", "."), - file_.split(".py")[0], - ) - yield mod_name - -# TODO: Disable walk through Eos paths until eos.types is killed. eos.types causes the import to break -''' -@pytest.mark.parametrize("mod_name", eos_modules()) -def test_eos_imports(mod_name): - assert importlib.import_module(mod_name) -''' diff --git a/tests/test_smoketests/test_rifter.py b/tests/test_smoketests/test_rifter.py new file mode 100644 index 000000000..b6bb63ca5 --- /dev/null +++ b/tests/test_smoketests/test_rifter.py @@ -0,0 +1,235 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..'))) + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +from _development.helpers_fits import RifterFit + + +# noinspection PyShadowingNames +def test_rifter_empty_char0(DB, Saveddata, RifterFit): + """ + We test an empty ship because if we use this as a base for testing our V skills, + and CCP ever fucks with the base states, all our derived stats will be wrong. + """ + char0 = Saveddata['Character'].getAll0() + + RifterFit.character = char0 + RifterFit.calculateModifiedAttributes() + + assert RifterFit.ship.getModifiedItemAttr("agility") == 3.2 + assert RifterFit.ship.getModifiedItemAttr("armorEmDamageResonance") == 0.4 + assert RifterFit.ship.getModifiedItemAttr("armorExplosiveDamageResonance") == 0.9 + assert RifterFit.ship.getModifiedItemAttr("armorHP") == 450.0 + assert RifterFit.ship.getModifiedItemAttr("armorKineticDamageResonance") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("armorThermalDamageResonance") == 0.65 + assert RifterFit.ship.getModifiedItemAttr("armorUniformity") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("baseWarpSpeed") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("capacitorCapacity") == 250.0 + assert RifterFit.ship.getModifiedItemAttr("capacity") == 140.0 + assert RifterFit.ship.getModifiedItemAttr("cpuLoad") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 130.0 + assert RifterFit.ship.getModifiedItemAttr("damage") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("droneBandwidth") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("droneCapacity") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("emDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("explosiveDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("fwLpKill") == 25.0 + assert RifterFit.ship.getModifiedItemAttr("gfxBoosterID") == 397.0 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationHi") == 0.63 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationLow") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationMed") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityHi") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityLow") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityMed") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateHi") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateLow") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateMed") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatGenerationMultiplier") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hiSlots") == 4.0 + assert RifterFit.ship.getModifiedItemAttr("hp") == 350.0 + assert RifterFit.ship.getModifiedItemAttr("hullEmDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullExplosiveDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullKineticDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullThermalDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("kineticDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("launcherSlotsLeft") == 2.0 + assert RifterFit.ship.getModifiedItemAttr("lowSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("mainColor") == 16777215.0 + assert RifterFit.ship.getModifiedItemAttr("mass") == 1067000.0 + assert RifterFit.ship.getModifiedItemAttr("maxDirectionalVelocity") == 3000.0 + assert RifterFit.ship.getModifiedItemAttr("maxLockedTargets") == 4.0 + assert RifterFit.ship.getModifiedItemAttr("maxPassengers") == 2.0 + assert RifterFit.ship.getModifiedItemAttr("maxTargetRange") == 22500.0 + assert RifterFit.ship.getModifiedItemAttr("maxVelocity") == 365.0 + assert RifterFit.ship.getModifiedItemAttr("medSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("metaLevel") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("minTargetVelDmgMultiplier") == 0.05 + assert RifterFit.ship.getModifiedItemAttr("powerLoad") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("powerOutput") == 41.0 + assert RifterFit.ship.getModifiedItemAttr("powerToSpeed") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("propulsionGraphicID") == 397.0 + assert RifterFit.ship.getModifiedItemAttr("radius") == 31.0 + assert RifterFit.ship.getModifiedItemAttr("rechargeRate") == 125000.0 + assert RifterFit.ship.getModifiedItemAttr("requiredSkill1") == 3329.0 + assert RifterFit.ship.getModifiedItemAttr("requiredSkill1Level") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("rigSize") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("rigSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("scanGravimetricStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanLadarStrength") == 8.0 + assert RifterFit.ship.getModifiedItemAttr("scanMagnetometricStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanRadarStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanResolution") == 660.0 + assert RifterFit.ship.getModifiedItemAttr("scanSpeed") == 1500.0 + assert RifterFit.ship.getModifiedItemAttr("shieldCapacity") == 450.0 + assert RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("shieldExplosiveDamageResonance") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("shieldKineticDamageResonance") == 0.6 + assert RifterFit.ship.getModifiedItemAttr("shieldRechargeRate") == 625000.0 + assert RifterFit.ship.getModifiedItemAttr("shieldThermalDamageResonance") == 0.8 + assert RifterFit.ship.getModifiedItemAttr("shieldUniformity") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("shipBonusMF") == 5.0 + assert RifterFit.ship.getModifiedItemAttr("shipBonusMF2") == 10.0 + assert RifterFit.ship.getModifiedItemAttr("shipScanResistance") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("signatureRadius") == 35.0 + assert RifterFit.ship.getModifiedItemAttr("structureUniformity") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("techLevel") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("thermalDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("turretSlotsLeft") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("typeColorScheme") == 11342.0 + assert RifterFit.ship.getModifiedItemAttr("uniformity") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("upgradeCapacity") == 400.0 + assert RifterFit.ship.getModifiedItemAttr("upgradeSlotsLeft") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("volume") == 27289.0 + assert RifterFit.ship.getModifiedItemAttr("warpCapacitorNeed") == 2.24e-06 + assert RifterFit.ship.getModifiedItemAttr("warpFactor") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("warpSpeedMultiplier") == 5.0 + + +# noinspection PyShadowingNames +def test_rifter_empty_char5(DB, Saveddata, RifterFit): + """ + Test char skills applying to a ship + """ + char5 = Saveddata['Character'].getAll5() + + RifterFit.character = char5 + RifterFit.calculateModifiedAttributes() + + assert RifterFit.ship.getModifiedItemAttr("agility") == 2.16 + assert RifterFit.ship.getModifiedItemAttr("armorEmDamageResonance") == 0.4 + assert RifterFit.ship.getModifiedItemAttr("armorExplosiveDamageResonance") == 0.9 + assert RifterFit.ship.getModifiedItemAttr("armorHP") == 562.5 + assert RifterFit.ship.getModifiedItemAttr("armorKineticDamageResonance") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("armorThermalDamageResonance") == 0.65 + assert RifterFit.ship.getModifiedItemAttr("armorUniformity") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("baseWarpSpeed") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("capacitorCapacity") == 312.5 + assert RifterFit.ship.getModifiedItemAttr("capacity") == 140.0 + assert RifterFit.ship.getModifiedItemAttr("cpuLoad") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 162.5 + assert RifterFit.ship.getModifiedItemAttr("damage") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("droneBandwidth") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("droneCapacity") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("emDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("explosiveDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("fwLpKill") == 25.0 + assert RifterFit.ship.getModifiedItemAttr("gfxBoosterID") == 397.0 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationHi") == 0.63 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationLow") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationMed") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityHi") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityLow") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityMed") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateHi") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateLow") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateMed") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatGenerationMultiplier") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hiSlots") == 4.0 + assert RifterFit.ship.getModifiedItemAttr("hp") == 437.5 + assert RifterFit.ship.getModifiedItemAttr("hullEmDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullExplosiveDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullKineticDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullThermalDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("kineticDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("launcherSlotsLeft") == 2.0 + assert RifterFit.ship.getModifiedItemAttr("lowSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("mainColor") == 16777215.0 + assert RifterFit.ship.getModifiedItemAttr("mass") == 1067000.0 + assert RifterFit.ship.getModifiedItemAttr("maxDirectionalVelocity") == 3000.0 + assert RifterFit.ship.getModifiedItemAttr("maxLockedTargets") == 4.0 + assert RifterFit.ship.getModifiedItemAttr("maxPassengers") == 2.0 + assert RifterFit.ship.getModifiedItemAttr("maxTargetRange") == 28125.0 + assert RifterFit.ship.getModifiedItemAttr("maxVelocity") == 456.25 + assert RifterFit.ship.getModifiedItemAttr("medSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("metaLevel") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("minTargetVelDmgMultiplier") == 0.05 + assert RifterFit.ship.getModifiedItemAttr("powerLoad") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("powerOutput") == 51.25 + assert RifterFit.ship.getModifiedItemAttr("powerToSpeed") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("propulsionGraphicID") == 397.0 + assert RifterFit.ship.getModifiedItemAttr("radius") == 31.0 + assert RifterFit.ship.getModifiedItemAttr("rechargeRate") == 93750.0 + assert RifterFit.ship.getModifiedItemAttr("requiredSkill1") == 3329.0 + assert RifterFit.ship.getModifiedItemAttr("requiredSkill1Level") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("rigSize") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("rigSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("scanGravimetricStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanLadarStrength") == 9.6 + assert RifterFit.ship.getModifiedItemAttr("scanMagnetometricStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanRadarStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanResolution") == 825.0 + assert RifterFit.ship.getModifiedItemAttr("scanSpeed") == 1500.0 + assert RifterFit.ship.getModifiedItemAttr("shieldCapacity") == 562.5 + assert RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("shieldExplosiveDamageResonance") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("shieldKineticDamageResonance") == 0.6 + assert RifterFit.ship.getModifiedItemAttr("shieldRechargeRate") == 468750.0 + assert RifterFit.ship.getModifiedItemAttr("shieldThermalDamageResonance") == 0.8 + assert RifterFit.ship.getModifiedItemAttr("shieldUniformity") == 1 + assert RifterFit.ship.getModifiedItemAttr("shipBonusMF") == 5.0 + assert RifterFit.ship.getModifiedItemAttr("shipBonusMF2") == 10.0 + assert RifterFit.ship.getModifiedItemAttr("shipScanResistance") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("signatureRadius") == 35.0 + assert RifterFit.ship.getModifiedItemAttr("structureUniformity") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("techLevel") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("thermalDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("turretSlotsLeft") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("typeColorScheme") == 11342.0 + assert RifterFit.ship.getModifiedItemAttr("uniformity") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("upgradeCapacity") == 400.0 + assert RifterFit.ship.getModifiedItemAttr("upgradeSlotsLeft") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("volume") == 27289.0 + assert RifterFit.ship.getModifiedItemAttr("warpCapacitorNeed") == 1.12e-06 + assert RifterFit.ship.getModifiedItemAttr("warpFactor") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("warpSpeedMultiplier") == 5.0 + + +# noinspection PyShadowingNames +def test_rifter_coprocessor(DB, Saveddata, RifterFit): + char5 = Saveddata['Character'].getAll5() + char0 = Saveddata['Character'].getAll0() + + RifterFit.character = char0 + mod = Saveddata['Module'](DB['db'].getItem("Co-Processor II")) + mod.state = Saveddata['State'].OFFLINE + RifterFit.modules.append(mod) + + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 130 + + RifterFit.calculateModifiedAttributes() + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 130 + + mod.state = Saveddata['State'].ONLINE + RifterFit.clear() + RifterFit.calculateModifiedAttributes() + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 143 + + RifterFit.character = char5 + RifterFit.clear() + RifterFit.calculateModifiedAttributes() + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 178.75