diff --git a/eos/db/gamedata/item.py b/eos/db/gamedata/item.py index df7508e43..fd7be477d 100644 --- a/eos/db/gamedata/item.py +++ b/eos/db/gamedata/item.py @@ -40,7 +40,9 @@ items_table = Table("invtypes", gamedata_meta, Column("marketGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID")), Column("iconID", Integer), Column("graphicID", Integer), - Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True)) + Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True), + Column("replaceSame", String), + Column("replaceBetter", String)) from .metaGroup import metatypes_table # noqa from .traits import traits_table # noqa diff --git a/eos/db/migrations/upgrade30.py b/eos/db/migrations/upgrade30.py new file mode 100644 index 000000000..7954f2d37 --- /dev/null +++ b/eos/db/migrations/upgrade30.py @@ -0,0 +1,17 @@ +""" +Migration 30 + +- changes to prices table +""" + + +import sqlalchemy + + +def upgrade(saveddata_engine): + try: + saveddata_engine.execute("SELECT status FROM prices LIMIT 1") + except sqlalchemy.exc.DatabaseError: + # Just drop table, table will be re-created by sqlalchemy and + # data will be re-fetched + saveddata_engine.execute("DROP TABLE prices;") diff --git a/eos/db/saveddata/price.py b/eos/db/saveddata/price.py index 8be7f6519..8abd07132 100644 --- a/eos/db/saveddata/price.py +++ b/eos/db/saveddata/price.py @@ -17,17 +17,20 @@ # along with eos. If not, see . # =============================================================================== + from sqlalchemy import Table, Column, Float, Integer from sqlalchemy.orm import mapper from eos.db import saveddata_meta from eos.saveddata.price import Price + prices_table = Table("prices", saveddata_meta, Column("typeID", Integer, primary_key=True), Column("price", Float, default=0.0), Column("time", Integer, nullable=False), - Column("failed", Integer)) + Column("status", Integer, nullable=False)) + mapper(Price, prices_table, properties={ "_Price__price": prices_table.c.price, diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index e582eef87..448584420 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -542,8 +542,17 @@ def commit(): with sd_lock: try: saveddata_session.commit() - saveddata_session.flush() - except Exception as ex: + except Exception: + saveddata_session.rollback() + exc_info = sys.exc_info() + raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) + + +def flush(): + with sd_lock: + try: + saveddata_session.flush() + except Exception: saveddata_session.rollback() exc_info = sys.exc_info() raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) diff --git a/eos/effects/ammoinfluencecapneed.py b/eos/effects/ammoinfluencecapneed.py index 587e55cd5..3bbb1103c 100644 --- a/eos/effects/ammoinfluencecapneed.py +++ b/eos/effects/ammoinfluencecapneed.py @@ -1,7 +1,7 @@ # ammoInfluenceCapNeed # # Used by: -# Items from category: Charge (493 of 947) +# Items from category: Charge (493 of 949) type = "passive" diff --git a/eos/effects/ammoinfluencerange.py b/eos/effects/ammoinfluencerange.py index 4358a6ff9..58c331911 100644 --- a/eos/effects/ammoinfluencerange.py +++ b/eos/effects/ammoinfluencerange.py @@ -1,7 +1,7 @@ # ammoInfluenceRange # # Used by: -# Items from category: Charge (587 of 947) +# Items from category: Charge (587 of 949) type = "passive" diff --git a/eos/effects/ammospeedmultiplier.py b/eos/effects/ammospeedmultiplier.py index 2a6c2e4d3..093953c11 100644 --- a/eos/effects/ammospeedmultiplier.py +++ b/eos/effects/ammospeedmultiplier.py @@ -1,10 +1,9 @@ # ammoSpeedMultiplier # # Used by: -# Charges from group: Festival Charges (23 of 23) +# Charges from group: Festival Charges (26 of 26) # Charges from group: Interdiction Probe (2 of 2) -# Charges from group: Structure Festival Charges (3 of 3) -# Special Edition Assetss from group: Festival Charges Expired (2 of 2) +# Items from market group: Special Edition Assets > Special Edition Festival Assets (30 of 33) type = "passive" diff --git a/eos/effects/ammotrackingmultiplier.py b/eos/effects/ammotrackingmultiplier.py index 6153af15c..95033d743 100644 --- a/eos/effects/ammotrackingmultiplier.py +++ b/eos/effects/ammotrackingmultiplier.py @@ -1,7 +1,7 @@ # ammoTrackingMultiplier # # Used by: -# Items from category: Charge (182 of 947) +# Items from category: Charge (182 of 949) # Charges from group: Projectile Ammo (128 of 128) type = "passive" diff --git a/eos/effects/boostershieldcapacitypenalty.py b/eos/effects/boostershieldcapacitypenalty.py index 41108747f..f93f04af9 100644 --- a/eos/effects/boostershieldcapacitypenalty.py +++ b/eos/effects/boostershieldcapacitypenalty.py @@ -1,7 +1,7 @@ # boosterShieldCapacityPenalty # # Used by: -# Implants from group: Booster (12 of 71) +# Implants from group: Booster (12 of 70) type = "boosterSideEffect" # User-friendly name for the side effect diff --git a/eos/effects/cynosuraldurationbonus.py b/eos/effects/cynosuraldurationbonus.py index 4f78ea09e..defbd9ca3 100644 --- a/eos/effects/cynosuraldurationbonus.py +++ b/eos/effects/cynosuraldurationbonus.py @@ -6,5 +6,5 @@ type = "passive" def handler(fit, ship, context): - fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field", + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field Generator", "duration", ship.getModifiedItemAttr("durationBonus")) diff --git a/eos/effects/cynosuralgeneration.py b/eos/effects/cynosuralgeneration.py index b99325c0f..36e29c52d 100644 --- a/eos/effects/cynosuralgeneration.py +++ b/eos/effects/cynosuralgeneration.py @@ -1,7 +1,7 @@ # cynosuralGeneration # # Used by: -# Modules from group: Cynosural Field (2 of 2) +# Modules from group: Cynosural Field Generator (2 of 2) type = "active" diff --git a/eos/effects/cynosuraltheoryconsumptionbonus.py b/eos/effects/cynosuraltheoryconsumptionbonus.py index d67c969f8..d6bda31b7 100644 --- a/eos/effects/cynosuraltheoryconsumptionbonus.py +++ b/eos/effects/cynosuraltheoryconsumptionbonus.py @@ -8,6 +8,6 @@ type = "passive" def handler(fit, container, context): level = container.level if "skill" in context else 1 - fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field", + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Cynosural Field Generator", "consumptionQuantity", container.getModifiedItemAttr("consumptionQuantityBonusPercentage") * level) diff --git a/eos/effects/implantwarpscramblerangebonus.py b/eos/effects/implantwarpscramblerangebonus.py new file mode 100644 index 000000000..8eea16959 --- /dev/null +++ b/eos/effects/implantwarpscramblerangebonus.py @@ -0,0 +1,10 @@ +# implantWarpScrambleRangeBonus +# +# Used by: +# Implants named like: Inquest 'Hedone' Entanglement Optimizer WS (3 of 3) +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Warp Scrambler", "maxRange", + src.getModifiedItemAttr("warpScrambleRangeBonus"), stackingPenalties=False) diff --git a/eos/effects/miningdurationmultiplieronline.py b/eos/effects/miningdurationmultiplieronline.py new file mode 100644 index 000000000..3d4cad5a5 --- /dev/null +++ b/eos/effects/miningdurationmultiplieronline.py @@ -0,0 +1,10 @@ +# miningDurationMultiplierOnline +# +# Used by: +# Module: Frostline 'Omnivore' Harvester Upgrade +type = "passive" + + +def handler(fit, module, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Mining Laser", + "duration", module.getModifiedItemAttr("miningDurationMultiplier")) diff --git a/eos/effects/overloadrofbonus.py b/eos/effects/overloadrofbonus.py index b362b9cc5..c0fedef1a 100644 --- a/eos/effects/overloadrofbonus.py +++ b/eos/effects/overloadrofbonus.py @@ -2,7 +2,7 @@ # # Used by: # Modules from group: Missile Launcher Torpedo (22 of 22) -# Items from market group: Ship Equipment > Turrets & Bays (429 of 882) +# Items from market group: Ship Equipment > Turrets & Bays (429 of 883) # Module: Interdiction Sphere Launcher I type = "overheat" diff --git a/eos/effects/remotewebifiermaxrangebonus.py b/eos/effects/remotewebifiermaxrangebonus.py index 46e5887dd..531f18ded 100644 --- a/eos/effects/remotewebifiermaxrangebonus.py +++ b/eos/effects/remotewebifiermaxrangebonus.py @@ -2,6 +2,7 @@ # # Used by: # Implants named like: Inquest 'Eros' Stasis Webifier MR (3 of 3) +# Implants named like: Inquest 'Hedone' Entanglement Optimizer WS (3 of 3) type = "passive" diff --git a/eos/effects/shipbonusnosneutcapneedrolebonus2.py b/eos/effects/shipbonusnosneutcapneedrolebonus2.py index 314736818..ec427ec11 100644 --- a/eos/effects/shipbonusnosneutcapneedrolebonus2.py +++ b/eos/effects/shipbonusnosneutcapneedrolebonus2.py @@ -1,8 +1,7 @@ # shipBonusNosNeutCapNeedRoleBonus2 # # Used by: -# Ship: Rodiva -# Ship: Zarmazd +# Variations of ship: Rodiva (2 of 2) type = "passive" def handler(fit, src, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capacitor Emission Systems"), "capacitorNeed", src.getModifiedItemAttr("shipBonusRole2")) diff --git a/eos/effects/shipbonusremotecapacitortransferrangerole1.py b/eos/effects/shipbonusremotecapacitortransferrangerole1.py index 22e7efe3b..bfd670862 100644 --- a/eos/effects/shipbonusremotecapacitortransferrangerole1.py +++ b/eos/effects/shipbonusremotecapacitortransferrangerole1.py @@ -1,8 +1,7 @@ # shipBonusRemoteCapacitorTransferRangeRole1 # # Used by: -# Ship: Rodiva -# Ship: Zarmazd +# Variations of ship: Rodiva (2 of 2) type = "passive" def handler(fit, src, context): fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Remote Capacitor Transmitter", "maxRange", src.getModifiedItemAttr("shipBonusRole1")) diff --git a/eos/effects/shipbonussmartbombcapneedrolebonus2.py b/eos/effects/shipbonussmartbombcapneedrolebonus2.py index 71df39ec4..69e4017a8 100644 --- a/eos/effects/shipbonussmartbombcapneedrolebonus2.py +++ b/eos/effects/shipbonussmartbombcapneedrolebonus2.py @@ -1,15 +1,14 @@ # shipBonusSmartbombCapNeedRoleBonus2 # # Used by: +# Variations of ship: Rodiva (2 of 2) # Ship: Damavik # Ship: Drekavac # Ship: Hydra # Ship: Kikimora # Ship: Leshak -# Ship: Rodiva # Ship: Tiamat # Ship: Vedmak -# Ship: Zarmazd type = "passive" diff --git a/eos/effects/skillbombdeploymentmodulereactivationdelaybonus.py b/eos/effects/skillbombdeploymentmodulereactivationdelaybonus.py index e56b2fb94..4db900c9b 100644 --- a/eos/effects/skillbombdeploymentmodulereactivationdelaybonus.py +++ b/eos/effects/skillbombdeploymentmodulereactivationdelaybonus.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, skill, context): fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Missile Launcher Bomb", - "moduleReactivationDelay", skill.getModifiedItemAttr("rofBonus") * skill.level) + "moduleReactivationDelay", skill.getModifiedItemAttr("reactivationDelayBonus") * skill.level) diff --git a/eos/effects/skillbonusdronedurability.py b/eos/effects/skillbonusdronedurability.py index e68cb4254..f36e82a3f 100644 --- a/eos/effects/skillbonusdronedurability.py +++ b/eos/effects/skillbonusdronedurability.py @@ -1,7 +1,6 @@ # skillBonusDroneDurability # # Used by: -# Implants from group: Cyber Drones (4 of 4) # Skill: Drone Durability type = "passive" diff --git a/eos/effects/skillbonusdronedurabilitynotfighters.py b/eos/effects/skillbonusdronedurabilitynotfighters.py new file mode 100644 index 000000000..d2b8a0967 --- /dev/null +++ b/eos/effects/skillbonusdronedurabilitynotfighters.py @@ -0,0 +1,14 @@ +# skillBonusDroneDurabilityNotFighters +# +# Used by: +# Implants from group: Cyber Drones (4 of 4) +type = "passive" + + +def handler(fit, src, context): + fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "hp", + src.getModifiedItemAttr("hullHpBonus")) + fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "armorHP", + src.getModifiedItemAttr("armorHpBonus")) + fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "shieldCapacity", + src.getModifiedItemAttr("shieldCapacityBonus")) diff --git a/eos/effects/skillbonusdroneinterfacing.py b/eos/effects/skillbonusdroneinterfacing.py index 93093efc3..d832d432b 100644 --- a/eos/effects/skillbonusdroneinterfacing.py +++ b/eos/effects/skillbonusdroneinterfacing.py @@ -1,8 +1,6 @@ # skillBonusDroneInterfacing # # Used by: -# Implant: CreoDron 'Bumblebee' Drone Tuner T10-5D -# Implant: CreoDron 'Yellowjacket' Drone Tuner D5-10T # Skill: Drone Interfacing type = "passive" diff --git a/eos/effects/skillbonusdroneinterfacingnotfighters.py b/eos/effects/skillbonusdroneinterfacingnotfighters.py new file mode 100644 index 000000000..01ab85de7 --- /dev/null +++ b/eos/effects/skillbonusdroneinterfacingnotfighters.py @@ -0,0 +1,11 @@ +# skillBonusDroneInterfacingNotFighters +# +# Used by: +# Implant: CreoDron 'Bumblebee' Drone Tuner T10-5D +# Implant: CreoDron 'Yellowjacket' Drone Tuner D5-10T +type = "passive" + + +def handler(fit, src, context): + fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "damageMultiplier", + src.getModifiedItemAttr("damageMultiplierBonus")) diff --git a/eos/effects/stripminerdurationmultiplier.py b/eos/effects/stripminerdurationmultiplier.py new file mode 100644 index 000000000..c9d0179a6 --- /dev/null +++ b/eos/effects/stripminerdurationmultiplier.py @@ -0,0 +1,10 @@ +# stripMinerDurationMultiplier +# +# Used by: +# Module: Frostline 'Omnivore' Harvester Upgrade +type = "passive" + + +def handler(fit, module, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Strip Miner", + "duration", module.getModifiedItemAttr("miningDurationMultiplier")) diff --git a/eos/gamedata.py b/eos/gamedata.py index f8345ac87..5ec2a1e14 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -239,7 +239,7 @@ class Item(EqBase): self.__offensive = None self.__assistive = None self.__overrides = None - self.__price = None + self.__priceObj = None @property def attributes(self): @@ -446,34 +446,33 @@ class Item(EqBase): @property def price(self): - # todo: use `from sqlalchemy import inspect` instead (mac-deprecated doesn't have inspect(), was imp[lemented in 0.8) - if self.__price is not None and getattr(self.__price, '_sa_instance_state', None) and self.__price._sa_instance_state.deleted: + if self.__priceObj is not None and getattr(self.__priceObj, '_sa_instance_state', None) and self.__priceObj._sa_instance_state.deleted: pyfalog.debug("Price data for {} was deleted (probably from a cache reset), resetting object".format(self.ID)) - self.__price = None + self.__priceObj = None - if self.__price is None: + if self.__priceObj is None: db_price = eos.db.getPrice(self.ID) # do not yet have a price in the database for this item, create one if db_price is None: pyfalog.debug("Creating a price for {}".format(self.ID)) - self.__price = types_Price(self.ID) - eos.db.add(self.__price) - eos.db.commit() + self.__priceObj = types_Price(self.ID) + eos.db.add(self.__priceObj) + eos.db.flush() else: - self.__price = db_price + self.__priceObj = db_price - return self.__price + return self.__priceObj @property def isAbyssal(self): if Item.ABYSSAL_TYPES is None: - Item.getAbyssalYypes() + Item.getAbyssalTypes() return self.ID in Item.ABYSSAL_TYPES @classmethod - def getAbyssalYypes(cls): + def getAbyssalTypes(cls): cls.ABYSSAL_TYPES = eos.db.getAbyssalTypes() @property diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py index d660dd38b..756a9f806 100644 --- a/eos/modifiedAttributeDict.py +++ b/eos/modifiedAttributeDict.py @@ -215,6 +215,8 @@ class ModifiedAttributeDict(collections.MutableMapping): if force is not None: if cappingValue is not None: force = min(force, cappingValue) + if key in (50, 30, 48, 11): + force = round(force, 2) return force # Grab our values if they're there, otherwise we'll take default values preIncrease = self.__preIncreases.get(key, 0) @@ -268,7 +270,8 @@ class ModifiedAttributeDict(collections.MutableMapping): # Cap value if we have cap defined if cappingValue is not None: val = min(val, cappingValue) - + if key in (50, 30, 48, 11): + val = round(val, 2) return val def __handleSkill(self, skillName): diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 4373bcdd0..f33a78651 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -1016,11 +1016,11 @@ class Fit(object): @property def pgUsed(self): - return self.getItemAttrOnlineSum(self.modules, "power") + return round(self.getItemAttrOnlineSum(self.modules, "power"), 2) @property def cpuUsed(self): - return self.getItemAttrOnlineSum(self.modules, "cpu") + return round(self.getItemAttrOnlineSum(self.modules, "cpu"), 2) @property def droneBandwidthUsed(self): diff --git a/eos/saveddata/price.py b/eos/saveddata/price.py index 6618119d0..a2a630c30 100644 --- a/eos/saveddata/price.py +++ b/eos/saveddata/price.py @@ -18,24 +18,30 @@ # along with eos. If not, see . # =============================================================================== -import time -from sqlalchemy.orm import reconstructor +import time +from enum import IntEnum, unique + from logbook import Logger + pyfalog = Logger(__name__) +@unique +class PriceStatus(IntEnum): + notFetched = 0 + success = 1 + fail = 2 + notSupported = 3 + + class Price(object): def __init__(self, typeID): self.typeID = typeID self.time = 0 self.__price = 0 - self.failed = None - - @reconstructor - def init(self): - self.__item = None + self.status = PriceStatus.notFetched @property def isValid(self): @@ -43,7 +49,10 @@ class Price(object): @property def price(self): - return self.__price or 0.0 + if self.status != PriceStatus.success: + return 0 + else: + return self.__price or 0 @price.setter def price(self, price): diff --git a/eve.db b/eve.db index fe0f660a1..417b53d1c 100644 Binary files a/eve.db and b/eve.db differ diff --git a/gui/builtinContextMenus/fillWithModule.py b/gui/builtinContextMenus/fillWithModule.py index 9e32dbc0d..0dd8d0d91 100644 --- a/gui/builtinContextMenus/fillWithModule.py +++ b/gui/builtinContextMenus/fillWithModule.py @@ -13,6 +13,8 @@ class FillWithModule(ContextMenu): self.settings = ContextMenuSettings.getInstance() def display(self, srcContext, selection): + if not self.settings.get('moduleFill'): + return False return srcContext in ("fittingModule") def getText(self, itmContext, selection): diff --git a/gui/builtinContextMenus/marketJump.py b/gui/builtinContextMenus/marketJump.py index c630312c1..a5d5e8eeb 100644 --- a/gui/builtinContextMenus/marketJump.py +++ b/gui/builtinContextMenus/marketJump.py @@ -26,7 +26,10 @@ class MarketJump(ContextMenu): sMkt = Market.getInstance() item = getattr(selection[0], "item", selection[0]) + isMutated = getattr(selection[0], "isMutated", False) mktGrp = sMkt.getMarketGroupByItem(item) + if mktGrp is None and isMutated: + mktGrp = sMkt.getMarketGroupByItem(selection[0].baseItem) # 1663 is Special Edition Festival Assets, we don't have root group for it if mktGrp is None or mktGrp.ID == 1663: @@ -43,7 +46,10 @@ class MarketJump(ContextMenu): if srcContext in ("fittingCharge", "projectedCharge"): item = selection[0].charge elif hasattr(selection[0], "item"): - item = selection[0].item + if getattr(selection[0], "isMutated", False): + item = selection[0].baseItem + else: + item = selection[0].item else: item = selection[0] diff --git a/gui/builtinItemStatsViews/attributeSlider.py b/gui/builtinItemStatsViews/attributeSlider.py index e45fa8f49..9e3585ea9 100644 --- a/gui/builtinItemStatsViews/attributeSlider.py +++ b/gui/builtinItemStatsViews/attributeSlider.py @@ -58,7 +58,6 @@ class AttributeSlider(wx.Panel): self.UserMinValue = minValue self.UserMaxValue = maxValue - print(self.UserMinValue, self.UserMaxValue) self.inverse = inverse diff --git a/gui/builtinItemStatsViews/itemCompare.py b/gui/builtinItemStatsViews/itemCompare.py index 1561ed58b..97a4b953d 100644 --- a/gui/builtinItemStatsViews/itemCompare.py +++ b/gui/builtinItemStatsViews/itemCompare.py @@ -8,6 +8,10 @@ from service.attribute import Attribute from gui.utils.numberFormatter import formatAmount +def defaultSort(item): + return (item.attributes['metaLevel'].value if 'metaLevel' in item.attributes else 0, item.name) + + class ItemCompare(wx.Panel): def __init__(self, parent, stuff, item, items, context=None): # Start dealing with Price stuff to get that thread going @@ -27,8 +31,7 @@ class ItemCompare(wx.Panel): self.currentSort = None self.sortReverse = False self.item = item - self.items = sorted(items, - key=lambda x: x.attributes['metaLevel'].value if 'metaLevel' in x.attributes else 0) + self.items = sorted(items, key=defaultSort) self.attrs = {} # get a dict of attrName: attrInfo of all unique attributes across all items @@ -126,10 +129,15 @@ class ItemCompare(wx.Panel): # starts at 0 while the list has the item name as column 0. attr = str(list(self.attrs.keys())[sort - 1]) func = lambda _val: _val.attributes[attr].value if attr in _val.attributes else 0.0 + # Clicked on a column that's not part of our array (price most likely) except IndexError: - # Clicked on a column that's not part of our array (price most likely) - self.sortReverse = False - func = lambda _val: _val.attributes['metaLevel'].value if 'metaLevel' in _val.attributes else 0.0 + # Price + if sort == len(self.attrs) + 1: + func = lambda i: i.price.price if i.price.price != 0 else float("Inf") + # Something else + else: + self.sortReverse = False + func = defaultSort self.items = sorted(self.items, key=func, reverse=self.sortReverse) @@ -160,7 +168,7 @@ class ItemCompare(wx.Panel): self.paramList.SetItem(i, x + 1, valueUnit) # Add prices - self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True)) + self.paramList.SetItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True) if item.price.price else "") self.paramList.RefreshRows() self.Layout() diff --git a/gui/builtinItemStatsViews/itemDescription.py b/gui/builtinItemStatsViews/itemDescription.py index 475b88788..9c62dcc78 100644 --- a/gui/builtinItemStatsViews/itemDescription.py +++ b/gui/builtinItemStatsViews/itemDescription.py @@ -15,7 +15,6 @@ class ItemDescription(wx.Panel): fgcolor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) self.description = wx.html.HtmlWindow(self) - if not item.description: return diff --git a/gui/builtinItemStatsViews/itemMutator.py b/gui/builtinItemStatsViews/itemMutator.py index cf7568350..3e9edc582 100644 --- a/gui/builtinItemStatsViews/itemMutator.py +++ b/gui/builtinItemStatsViews/itemMutator.py @@ -22,8 +22,23 @@ class ItemMutator(wx.Panel): self.item = item self.timer = None self.activeFit = gui.mainFrame.MainFrame.getInstance().getActiveFit() + + font = parent.GetFont() + font.SetWeight(wx.BOLD) + mainSizer = wx.BoxSizer(wx.VERTICAL) + sourceItemsSizer = wx.BoxSizer(wx.HORIZONTAL) + sourceItemsSizer.Add(BitmapLoader.getStaticBitmap(stuff.item.iconID, self, "icons"), 0, wx.LEFT, 5) + sourceItemsSizer.Add(BitmapLoader.getStaticBitmap(stuff.mutaplasmid.item.iconID, self, "icons"), 0, wx.LEFT, 0) + sourceItemShort = "{} {}".format(stuff.mutaplasmid.item.name.split(" ")[0], stuff.baseItem.name) + sourceItemText = wx.StaticText(self, wx.ID_ANY, sourceItemShort) + sourceItemText.SetFont(font) + sourceItemsSizer.Add(sourceItemText, 0, wx.LEFT, 10) + mainSizer.Add(sourceItemsSizer, 0, wx.TOP | wx.EXPAND, 10) + + mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.ALL | wx.EXPAND, 5) + self.goodColor = wx.Colour(96, 191, 0) self.badColor = wx.Colour(255, 64, 0) @@ -31,28 +46,33 @@ class ItemMutator(wx.Panel): for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName): # Format: [raw value, modifier applied to base raw value, display value] - range1 = (m.minValue, m.minMod, m.attribute.unit.SimplifyValue(m.minValue)) - range2 = (m.maxValue, m.maxMod, m.attribute.unit.SimplifyValue(m.maxValue)) + minRange = (m.minValue, m.attribute.unit.SimplifyValue(m.minValue)) + maxRange = (m.maxValue, m.attribute.unit.SimplifyValue(m.maxValue)) - if (m.highIsGood and range1[0] >= range2[0]) or (not m.highIsGood and range1[0] <= range2[0]): - betterRange = range1 - worseRange = range2 + if (m.highIsGood and minRange[0] >= maxRange[0]) or (not m.highIsGood and minRange[0] <= maxRange[0]): + betterRange = minRange + worseRange = maxRange else: - betterRange = range2 - worseRange = range1 + betterRange = maxRange + worseRange = minRange - if range1[2] >= range2[2]: - displayMaxRange = range1 - displayMinRange = range2 + if minRange[1] >= maxRange[1]: + displayMaxRange = minRange + displayMinRange = maxRange else: - displayMaxRange = range2 - displayMinRange = range1 + displayMaxRange = maxRange + displayMinRange = minRange + + # If base value is outside of mutation range, make sure that center of slider + # corresponds to the value which is closest available to actual base value. It's + # how EVE handles it + if m.minValue <= m.baseValue <= m.maxValue: + sliderBaseValue = m.baseValue + else: + sliderBaseValue = max(m.minValue, min(m.maxValue, m.baseValue)) headingSizer = wx.BoxSizer(wx.HORIZONTAL) - font = parent.GetFont() - font.SetWeight(wx.BOLD) - headingSizer.Add(BitmapLoader.getStaticBitmap(m.attribute.iconID, self, "icons"), 0, wx.RIGHT, 10) displayName = wx.StaticText(self, wx.ID_ANY, m.attribute.displayName) @@ -75,9 +95,9 @@ class ItemMutator(wx.Panel): mainSizer.Add(headingSizer, 0, wx.ALL | wx.EXPAND, 5) slider = AttributeSlider(parent=self, - baseValue=m.attribute.unit.SimplifyValue(m.baseValue), - minValue=displayMinRange[2], - maxValue=displayMaxRange[2], + baseValue=m.attribute.unit.SimplifyValue(sliderBaseValue), + minValue=displayMinRange[1], + maxValue=displayMaxRange[1], inverse=displayMaxRange is worseRange) slider.SetValue(m.attribute.unit.SimplifyValue(m.value), False) slider.Bind(EVT_VALUE_CHANGED, self.changeMutatedValue) diff --git a/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py b/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py index 458424dce..7c767c5fb 100644 --- a/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py +++ b/gui/builtinPreferenceViews/pyfaContextMenuPreferences.py @@ -81,6 +81,11 @@ class PFContextMenuPref(PreferenceView): 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) + self.rbBox8.Bind(wx.EVT_RADIOBOX, self.OnSetting8Change) + mainSizer.Add(rbSizerRow3, 1, wx.ALL | wx.EXPAND, 0) panel.SetSizer(mainSizer) @@ -107,6 +112,9 @@ class PFContextMenuPref(PreferenceView): def OnSetting7Change(self, event): self.settings.set('project', event.GetInt()) + def OnSetting8Change(self, event): + self.settings.set('moduleFill', event.GetInt()) + def getImage(self): return BitmapLoader.getBitmap("settings_menu", "gui") diff --git a/gui/builtinViewColumns/misc.py b/gui/builtinViewColumns/misc.py index 85a0d545b..e9bff3d56 100644 --- a/gui/builtinViewColumns/misc.py +++ b/gui/builtinViewColumns/misc.py @@ -504,7 +504,7 @@ class Miscellanea(ViewColumn): text = "{0}s".format(cycleTime) tooltip = "Spoolup time" return text, tooltip - elif itemGroup in ("Siege Module", "Cynosural Field"): + elif itemGroup in ("Siege Module", "Cynosural Field Generator"): amt = stuff.getModifiedItemAttr("consumptionQuantity") if amt: typeID = stuff.getModifiedItemAttr("consumptionType") diff --git a/gui/builtinViewColumns/price.py b/gui/builtinViewColumns/price.py index a68614b83..56901c172 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.price import PriceStatus from service.price import Price as ServicePrice from gui.viewColumn import ViewColumn from gui.bitmap_loader import BitmapLoader @@ -45,13 +46,16 @@ class Price(ViewColumn): if stuff.isEmpty: return "" - price = stuff.item.price + priceObj = stuff.item.price - if not price or not price.isValid: + if not priceObj.isValid: return False # Fetch actual price as float to not modify its value on Price object - price = price.price + price = priceObj.price + + if price == 0: + return "" if isinstance(stuff, Drone) or isinstance(stuff, Cargo): price *= stuff.amount @@ -63,10 +67,12 @@ class Price(ViewColumn): def callback(item): price = item[0] - text = formatAmount(price.price, 3, 3, 9, currency=True) if price.price else "" - if price.failed: - text += " (!)" - colItem.SetText(text) + 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)) display.SetItem(colItem) diff --git a/gui/itemStats.py b/gui/itemStats.py index 3768780e2..50a02f065 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -91,7 +91,7 @@ class ItemStatsDialog(wx.Dialog): self.SetMinSize((300, 200)) if "wxGTK" in wx.PlatformInfo: # GTK has huge tab widgets, give it a bit more room - self.SetSize((580, 500)) + self.SetSize((630, 550)) else: self.SetSize((550, 500)) # self.SetMaxSize((500, -1)) @@ -168,8 +168,9 @@ class ItemStatsContainer(wx.Panel): self.mutator = ItemMutator(self.nbContainer, stuff, item) self.nbContainer.AddPage(self.mutator, "Mutations") - self.desc = ItemDescription(self.nbContainer, stuff, item) - self.nbContainer.AddPage(self.desc, "Description") + if item.description: + self.desc = ItemDescription(self.nbContainer, stuff, item) + self.nbContainer.AddPage(self.desc, "Description") self.params = ItemParams(self.nbContainer, stuff, item, context) self.nbContainer.AddPage(self.params, "Attributes") diff --git a/scripts/iconIDs.yaml b/scripts/iconIDs.yaml index 1fec32850..a2bdfc349 100644 --- a/scripts/iconIDs.yaml +++ b/scripts/iconIDs.yaml @@ -706,7 +706,7 @@ - tech2 iconFile: res:/ui/texture/icons/12_64_13.png 398: - description: Corpse floating in space (male?). - Corpse + description: chemical item iconFile: res:/ui/texture/icons/11_64_5.png 400: description: Mineral - Pyerite @@ -10432,6 +10432,66 @@ 22076: description: Gift Box with Ribbon iconFile: res:/ui/texture/icons/76_64_3.png +22077: + description: 49978_Male_outer_ExplorationSuit_M01_Types_ExplorationSuit_M01_W.png + iconFile: res:/UI/Asset/mannequin/outer/49978_Male_outer_ExplorationSuit_M01_Types_ExplorationSuit_M01_W.png +22078: + description: 49980_Female_Outer_ExplorationSuit_F01_Types_ExplorationSuit_F01_W.png + iconFile: res:/UI/Asset/mannequin/outer/49980_Female_Outer_ExplorationSuit_F01_Types_ExplorationSuit_F01_W.png +22079: + description: Preview for reward track augmentations + iconFile: res:/UI/Texture/Icons/RewardTrack/reward_Holiday2018_Augmentation1.png +22080: + description: Preview for reward track augmentations + iconFile: res:/UI/Texture/Icons/RewardTrack/reward_Holiday2018_Augmentation2.png +22081: + description: Holiday'18 Crate - Drake/Rupture Splash + iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_DrakeRupture.png +22082: + description: Holiday'18 Crate - ExpSuits + iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_ExplorationSuits.png +22084: + description: Holiday'18 Crate - Augmentation Set 1 + iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_FaceAugmentation1.png +22085: + description: Holiday'18 Crate - Augmentation Set 2 + iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_FaceAugmentation2.png +22086: + description: Holiday'18 Crate - Gnosis/Punisher Splash + iconFile: res:/UI/Texture/classes/ItemPacks/SplashImages/Holiday2018/splash_Holiday2018_GnosisPunisher.png +22087: + description: 49978_Male_outer_ExplorationSuit_M01_Types_ExplorationSuit_M01_W.png + iconFile: res:/UI/Asset/mannequin/outer/49978_Male_outer_ExplorationSuit_M01_Types_ExplorationSuit_M01_W.png +22088: + description: 49980_Female_Outer_ExplorationSuit_F01_Types_ExplorationSuit_F01_W.png + iconFile: res:/UI/Asset/mannequin/outer/49980_Female_Outer_ExplorationSuit_F01_Types_ExplorationSuit_F01_W.png +22089: + description: 49984_Female_Makeup_Augmentations_Face_Paint_F01_Types_Face_Paint_F01_V0_Blue.png + iconFile: res:/UI/Asset/mannequin/makeup_augmentations/49984_Female_Makeup_Augmentations_Face_Paint_F01_Types_Face_Paint_F01_V0_Blue.png +22090: + description: 49985_Female_Makeup_Augmentations_Face_Paint_F01_Types_Face_Paint_F01_V10_W.png + iconFile: res:/UI/Asset/mannequin/makeup_augmentations/49985_Female_Makeup_Augmentations_Face_Paint_F01_Types_Face_Paint_F01_V10_W.png 22091: - description: Manually added mutadaptive RR icon path - iconFile: res:/ui/texture/icons/modules/abyssalremoterepairer.png + backgrounds: + - blueprint + - blueprintCopy + description: Mutadaptive Remote Repairer + foregrounds: + - faction + - tech2 + iconFile: res:/ui/texture/icons/modules/abyssalRemoteRepairer.png +22092: + description: 49987_Male_Makeup_Augmentations_Face_Paint_M01_Types_Face_Paint_M01_V6_Blue.png + iconFile: res:/UI/Asset/mannequin/makeup_augmentations/49987_Male_Makeup_Augmentations_Face_Paint_M01_Types_Face_Paint_M01_V6_Blue.png +22093: + description: Generic SKIN icon for crates containing multiple SKINs + iconFile: res:/UI/Texture/Icons/RewardTrack/crateSkinContainer.png +22094: + description: Preview icon for exploration suit reward track winter'18 + iconFile: res:/UI/Texture/Icons/RewardTrack/crateWinterExplorationSuit.png +22095: + description: 49986_male_Makeup_Augmentations_Face_Paint_M01_Types_Face_Paint_M01_V10_W.png + iconFile: res:/UI/Asset/mannequin/makeup_augmentations/49986_male_Makeup_Augmentations_Face_Paint_M01_Types_Face_Paint_M01_V10_W.png +22096: + description: Winter Login Campaign Banner + iconFile: res:/UI/Texture/LoginCampaigns/Winter_2018.png diff --git a/scripts/jsonToSql.py b/scripts/jsonToSql.py index 39e51d198..913b74b1c 100755 --- a/scripts/jsonToSql.py +++ b/scripts/jsonToSql.py @@ -28,6 +28,8 @@ sys.path.insert(0, os.path.realpath(os.path.join(path, '..'))) import json import argparse +import itertools + CATEGORIES_TO_REMOVE = [ 30 # Apparel @@ -174,6 +176,119 @@ def main(db, json_path): newData.append(newRow) return newData + def fillReplacements(tables): + + def compareAttrs(attrs1, attrs2, attrHig): + """ + Compares received attribute sets. Returns: + - 0 if sets are different + - 1 if sets are exactly the same + - 2 if first set is strictly better + - 3 if second set is strictly better + """ + if set(attrs1) != set(attrs2): + return 0 + if all(attrs1[aid] == attrs2[aid] for aid in attrs1): + return 1 + if all( + (attrs1[aid] >= 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 + + skillReqAttribs = { + 182: 277, + 183: 278, + 184: 279, + 1285: 1286, + 1289: 1287, + 1290: 1288} + skillReqAttribsFlat = set(skillReqAttribs.keys()).union(skillReqAttribs.values()) + # Get data on type groups + typesGroups = {} + for row in tables['evetypes']: + typesGroups[row['typeID']] = row['groupID'] + # Get data on type attributes + typesNormalAttribs = {} + typesSkillAttribs = {} + for row in tables['dgmtypeattribs']: + attributeID = row['attributeID'] + if attributeID in skillReqAttribsFlat: + typeSkillAttribs = typesSkillAttribs.setdefault(row['typeID'], {}) + typeSkillAttribs[row['attributeID']] = row['value'] + # Ignore these attributes for comparison purposes + elif attributeID in ( + 422, # techLevel + 633, # metaLevel + 1692 # metaGroupID + ): + continue + else: + typeNormalAttribs = typesNormalAttribs.setdefault(row['typeID'], {}) + typeNormalAttribs[row['attributeID']] = row['value'] + # Get data on skill requirements + typesSkillReqs = {} + for typeID, typeAttribs in typesSkillAttribs.items(): + typeSkillAttribs = typesSkillAttribs.get(typeID, {}) + if not typeSkillAttribs: + continue + typeSkillReqs = typesSkillReqs.setdefault(typeID, {}) + for skillreqTypeAttr, skillreqLevelAttr in skillReqAttribs.items(): + try: + skillType = int(typeSkillAttribs[skillreqTypeAttr]) + skillLevel = int(typeSkillAttribs[skillreqLevelAttr]) + 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']) + # As EVE affects various types mostly depending on their group or skill requirements, + # we're going to group various types up this way + groupedData = {} + for row in tables['evetypes']: + typeID = row['typeID'] + typeAttribs = typesNormalAttribs.get(typeID, {}) + # Ignore stuff 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), []) + 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 + 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]) + # 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, ()))) + data = {} # Dump all data to memory so we can easely cross check ignored rows @@ -190,6 +305,8 @@ def main(db, json_path): tableData = convertClones(tableData) data[jsonName] = tableData + fillReplacements(data) + # Set with typeIDs which we will have in our database # Sometimes CCP unpublishes some items we want to have published, we # can do it here - just add them to initial set diff --git a/service/marketSources/evemarketdata.py b/service/marketSources/evemarketdata.py index bd1f8f3af..15edc92ef 100644 --- a/service/marketSources/evemarketdata.py +++ b/service/marketSources/evemarketdata.py @@ -22,6 +22,7 @@ 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 @@ -52,6 +53,7 @@ class EveMarketData(object): price = float(type_.firstChild.data) except (TypeError, ValueError): pyfalog.warning("Failed to get price for: {0}", type_) + continue # Fill price data priceobj = priceMap[typeID] @@ -61,11 +63,10 @@ class EveMarketData(object): if price != 0: priceobj.price = price priceobj.time = time.time() + VALIDITY + priceobj.status = PriceStatus.success else: priceobj.time = time.time() + TIMEOUT - priceobj.failed = None - # delete price from working dict del priceMap[typeID] diff --git a/service/marketSources/evemarketer.py b/service/marketSources/evemarketer.py index a2728ddfb..478de4371 100644 --- a/service/marketSources/evemarketer.py +++ b/service/marketSources/evemarketer.py @@ -22,13 +22,14 @@ 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 pyfalog = Logger(__name__) -class EveCentral(object): +class EveMarketer(object): name = "evemarketer" @@ -61,10 +62,10 @@ class EveCentral(object): priceobj = priceMap[typeID] priceobj.price = percprice priceobj.time = time.time() + VALIDITY - priceobj.failed = None + priceobj.status = PriceStatus.success # delete price from working dict del priceMap[typeID] -Price.register(EveCentral) +Price.register(EveMarketer) diff --git a/service/port/efs.py b/service/port/efs.py index 0e021902c..51ff5c518 100755 --- a/service/port/efs.py +++ b/service/port/efs.py @@ -1,13 +1,8 @@ -import inspect -import os -import platform -import re -import sys -import traceback import json import eos.db from math import log +from numbers import Number from config import version as pyfaVersion from service.fit import Fit from service.market import Market @@ -15,9 +10,11 @@ from eos.enum import Enum from eos.saveddata.module import Hardpoint, Slot, Module, State from eos.saveddata.drone import Drone from eos.effectHandlerHelpers import HandledList -from eos.db import gamedata_session, getItemsByCategory, getCategory, getAttributeInfo, getGroup -from eos.gamedata import Category, Group, Item, Traits, Attribute, Effect, ItemEffect +from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup +from eos.gamedata import Attribute, Effect, Group, Item, ItemEffect from eos.utils.spoolSupport import SpoolType, SpoolOptions +from gui.fitCommands.calc.fitAddModule import FitAddModuleCommand +from gui.fitCommands.calc.fitRemoveModule import FitRemoveModuleCommand from logbook import Logger pyfalog = Logger(__name__) @@ -30,9 +27,9 @@ class RigSize(Enum): CAPITAL = 4 -class EfsPort(): +class EfsPort: wepTestSet = {} - version = 0.02 + version = 0.03 @staticmethod def attrDirectMap(values, target, source): @@ -72,12 +69,12 @@ class EfsPort(): if propID is None: return None - sFit.appendModule(fitID, propID) + FitAddModuleCommand(fitID, propID).Do() sFit.recalc(fit) fit = eos.db.getFit(fitID) mwdPropSpeed = fit.maxSpeed mwdPosition = list(filter(lambda mod: mod.item and mod.item.ID == propID, fit.modules))[0].position - sFit.removeModule(fitID, mwdPosition) + FitRemoveModuleCommand(fitID, [mwdPosition]).Do() sFit.recalc(fit) fit = eos.db.getFit(fitID) return mwdPropSpeed @@ -115,6 +112,9 @@ class EfsPort(): "Titan Phenomena Generator", "Non-Repeating Hardeners" ] projectedMods = list(filter(lambda mod: mod.item and mod.item.group.name in modGroupNames, fit.modules)) + # Sort projections to prevent the order needlessly changing as pyfa updates. + projectedMods.sort(key=lambda mod: mod.item.ID) + projectedMods.sort(key=lambda mod: mod.item.group.ID) projections = [] for mod in projectedMods: maxRangeDefault = 0 @@ -191,7 +191,7 @@ class EfsPort(): return projections # Note that unless padTypeIDs is True all 0s will be removed from modTypeIDs in the return. - # They always are added initally for the sake of brevity, as this option may not be retained long term. + # They always are added initially for the sake of brevity, as this option may not be retained long term. @staticmethod def getModuleInfo(fit, padTypeIDs=False): moduleNames = [] @@ -303,9 +303,9 @@ class EfsPort(): def getWeaponSystemData(fit): weaponSystems = [] groups = {} - # TODO: fetch spoolup option + # Export at maximum spool for consistency, spoolup data is exported anyway. defaultSpoolValue = 1 - spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False) + spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, True) for mod in fit.modules: if mod.getDps(spoolOptions=spoolOptions).total > 0: # Group weapon + ammo combinations that occur more than once @@ -344,7 +344,7 @@ class EfsPort(): aoeFieldRange = stats.getModifiedItemAttr("empFieldRange") # This also covers non-bomb weapons with dps values and no hardpoints, most notably targeted doomsdays. typeing = "SmartBomb" - # Targeted DDs are the only non drone/fighter weapon without an explict max range + # Targeted DDs are the only non drone/fighter weapon without an explicit max range if stats.item.group.name == 'Super Weapon' and stats.maxRange is None: maxRange = 300000 else: @@ -515,7 +515,7 @@ class EfsPort(): # Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits. # standin class used to prevent . notation causing issues when used as an arg - class standin(): + class standin: pass tf = standin() tf.modules = HandledList(turrets + launchers) @@ -551,7 +551,7 @@ class EfsPort(): @staticmethod def getShipSize(groupID): - # Size groupings are somewhat arbitrary but allow for a more managable number of top level groupings in a tree structure. + # Size groupings are somewhat arbitrary but allow for a more manageable number of top level groupings in a tree structure. frigateGroupNames = ["Frigate", "Shuttle", "Corvette", "Assault Frigate", "Covert Ops", "Interceptor", "Stealth Bomber", "Electronic Attack Ship", "Expedition Frigate", "Logistics Frigate"] destroyerGroupNames = ["Destroyer", "Interdictor", "Tactical Destroyer", "Command Destroyer"] @@ -625,9 +625,29 @@ class EfsPort(): } resonance = {"hull": hullResonance, "armor": armorResonance, "shield": shieldResonance} shipSize = EfsPort.getShipSize(fit.ship.item.groupID) - # TODO: fetch spoolup option + # Export at maximum spool for consistency, spoolup data is exported anyway. defaultSpoolValue = 1 - spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False) + spoolOptions = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, True) + + def roundNumbers(data, digits): + if isinstance(data, str): + return + if isinstance(data, dict): + for key in data: + if isinstance(data[key], Number): + data[key] = round(data[key], digits) + else: + roundNumbers(data[key], digits) + if isinstance(data, list) or isinstance(data, tuple): + for val in data: + roundNumbers(val, digits) + if isinstance(data, Number): + rounded = round(data, digits) + if data != rounded: + pyfalog.error("Error rounding numbers for EFS export, export may be inconsistent." + "This suggests the format has been broken somewhere.") + return + try: dataDict = { "name": fitName, "ehp": fit.ehp, "droneDPS": fit.getDroneDps().total, @@ -650,9 +670,12 @@ class EfsPort(): "modTypeIDs": modTypeIDs, "moduleNames": moduleNames, "pyfaVersion": pyfaVersion, "efsExportVersion": EfsPort.version } - except TypeError: + # Recursively round any numbers in dicts to 6 decimal places. + # This prevents meaningless rounding errors from changing the output whenever pyfa changes. + roundNumbers(dataDict, 6) + except TypeError as e: pyfalog.error("Error parsing fit:" + str(fit)) - pyfalog.error(TypeError) + pyfalog.error(e) dataDict = {"name": fitName + "Fit could not be correctly parsed"} export = json.dumps(dataDict, skipkeys=True) return export diff --git a/service/price.py b/service/price.py index 9a8485274..3028b74ad 100644 --- a/service/price.py +++ b/service/price.py @@ -26,6 +26,7 @@ import wx from logbook import Logger from eos import db +from eos.saveddata.price import PriceStatus from service.fit import Fit from service.market import Market from service.network import TimeoutError @@ -85,13 +86,18 @@ class Price(object): toRequest = set() # Compose list of items we're going to request - for typeID in priceMap: + 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 - if item is not None and item.marketGroupID: - toRequest.add(typeID) + if item is None: + continue + if not item.marketGroupID: + priceMap[typeID].status = 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: @@ -117,11 +123,10 @@ class Price(object): except TimeoutError: # Timeout error deserves special treatment pyfalog.warning("Price fetch timout") - for typeID in priceMap.keys(): + for typeID in tuple(priceMap): priceobj = priceMap[typeID] priceobj.time = time.time() + TIMEOUT - priceobj.failed = True - + priceobj.status = PriceStatus.fail del priceMap[typeID] except Exception as ex: # something happened, try another source @@ -134,7 +139,7 @@ class Price(object): for typeID in priceMap.keys(): priceobj = priceMap[typeID] priceobj.time = time.time() + REREQUEST - priceobj.failed = True + priceobj.status = PriceStatus.fail @classmethod def fitItemsList(cls, fit): @@ -172,8 +177,8 @@ class Price(object): def getPrices(self, objitems, callback, waitforthread=False): """Get prices for multiple typeIDs""" requests = [] + sMkt = Market.getInstance() for objitem in objitems: - sMkt = Market.getInstance() item = sMkt.getItem(objitem) requests.append(item.price) @@ -197,6 +202,7 @@ class Price(object): class PriceWorkerThread(threading.Thread): + def __init__(self): threading.Thread.__init__(self) self.name = "PriceWorker" diff --git a/service/settings.py b/service/settings.py index 39f8def9c..df5038d1f 100644 --- a/service/settings.py +++ b/service/settings.py @@ -525,6 +525,7 @@ class ContextMenuSettings(object): "tacticalMode" : 1, "targetResists" : 1, "whProjector" : 1, + "moduleFill" : 1, } self.ContextMenuDefaultSettings = SettingsProvider.getInstance().getSettings("pyfaContextMenuSettings", ContextMenuDefaultSettings)