diff --git a/config.py b/config.py index 3ac5d9779..614aa9053 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,11 @@ import os import sys +# TODO: move all logging back to pyfa.py main loop +# We moved it here just to avoid rebuilding windows skeleton for now (any change to pyfa.py needs it) +import logging +import logging.handlers + # Load variable overrides specific to distribution type try: import configforced @@ -12,24 +17,21 @@ debug = False # Defines if our saveddata will be in pyfa root or not saveInRoot = False +logLevel = logging.WARN + # Version data -version = "1.12.1" -tag = "git" -expansionName = "Carnyx" +version = "1.13.2" +tag = "Stable" +expansionName = "Aegis" expansionVersion = "1.0" evemonMinVersion = "4081" -# Database version (int ONLY) -# Increment every time we need to flag for user database upgrade/modification -dbversion = 7 - pyfaPath = None savePath = None staticPath = None saveDB = None gameDB = None - def isFrozen(): if hasattr(sys, 'frozen'): return True @@ -43,6 +45,9 @@ def getPyfaRoot(): root = unicode(root, sys.getfilesystemencoding()) return root +def __createDirs(path): + if not os.path.exists(path): + os.makedirs(path) def defPaths(): global pyfaPath @@ -68,13 +73,22 @@ def defPaths(): savePath = unicode(os.path.expanduser(os.path.join("~", ".pyfa")), sys.getfilesystemencoding()) + __createDirs(savePath) + + format = '%(asctime)s %(name)-24s %(levelname)-8s %(message)s' + logging.basicConfig(format=format, level=logLevel) + handler = logging.handlers.RotatingFileHandler(os.path.join(savePath, "log.txt"), maxBytes=1000000, backupCount=3) + formatter = logging.Formatter(format) + handler.setFormatter(formatter) + logging.getLogger('').addHandler(handler) + + logging.info("Starting pyfa") + # Redirect stderr to file if we're requested to do so stderrToFile = getattr(configforced, "stderrToFile", None) if stderrToFile is None: stderrToFile = True if isFrozen() else False if stderrToFile is True: - if not os.path.exists(savePath): - os.mkdir(savePath) sys.stderr = open(os.path.join(savePath, "error_log.txt"), "w") # Same for stdout @@ -82,8 +96,6 @@ def defPaths(): if stdoutToFile is None: stdoutToFile = True if isFrozen() else False if stdoutToFile is True: - if not os.path.exists(savePath): - os.mkdir(savePath) sys.stdout = open(os.path.join(savePath, "output_log.txt"), "w") # Static EVE Data from the staticdata repository, should be in the staticdata diff --git a/eos/db/migration.py b/eos/db/migration.py index 92224c34c..b04df38ae 100644 --- a/eos/db/migration.py +++ b/eos/db/migration.py @@ -1,32 +1,46 @@ import config import shutil import time +import re +import os + +def getAppVersion(): + # calculate app version based on upgrade files we have + appVersion = 0 + for fname in os.listdir(os.path.join(os.path.dirname(__file__), "migrations")): + m = re.match("^upgrade(?P\d+)\.py$", fname) + if not m: + continue + index = int(m.group("index")) + appVersion = max(appVersion, index) + return appVersion def getVersion(db): cursor = db.execute('PRAGMA user_version') return cursor.fetchone()[0] def update(saveddata_engine): - currversion = getVersion(saveddata_engine) + dbVersion = getVersion(saveddata_engine) + appVersion = getAppVersion() - if currversion == config.dbversion: + if dbVersion == appVersion: return - if currversion < config.dbversion: + if dbVersion < appVersion: # Automatically backup database toFile = "%s/saveddata_migration_%d-%d_%s.db"%( config.savePath, - currversion, - config.dbversion, + dbVersion, + appVersion, time.strftime("%Y%m%d_%H%M%S")) shutil.copyfile(config.saveDB, toFile) - for version in xrange(currversion, config.dbversion): - module = __import__('eos.db.migrations.upgrade%d'%(version+1), fromlist=True) + for version in xrange(dbVersion, appVersion): + module = __import__("eos.db.migrations.upgrade{}".format(version + 1), fromlist=True) upgrade = getattr(module, "upgrade", False) if upgrade: upgrade(saveddata_engine) # when all is said and done, set version to current - saveddata_engine.execute('PRAGMA user_version = %d'%config.dbversion) + saveddata_engine.execute("PRAGMA user_version = {}".format(appVersion)) diff --git a/eos/db/migrations/upgrade8.py b/eos/db/migrations/upgrade8.py index 6097c3799..9d2c04321 100644 --- a/eos/db/migrations/upgrade8.py +++ b/eos/db/migrations/upgrade8.py @@ -1,13 +1,10 @@ """ -Migration 4 +Migration 8 -- Converts modules based on Proteus Module Tiericide +- Converts modules based on Carnyx Module Tiericide Some modules have been unpublished (and unpublished module attributes are removed from database), which causes pyfa to crash. We therefore replace these modules with their new replacements - - Based on http://community.eveonline.com/news/patch-notes/patch-notes-for-proteus/ - and output of itemDiff.py """ diff --git a/eos/db/migrations/upgrade9.py b/eos/db/migrations/upgrade9.py new file mode 100644 index 000000000..ae7b66ad5 --- /dev/null +++ b/eos/db/migrations/upgrade9.py @@ -0,0 +1,23 @@ +""" +Migration 9 + +Effectively drops UNIQUE constraint from boosters table. SQLite does not support +this, so we have to copy the table to the updated schema and then rename it +""" + +tmpTable = """ +CREATE TABLE boostersTemp ( + 'ID' INTEGER NOT NULL, + 'itemID' INTEGER, + 'fitID' INTEGER NOT NULL, + 'active' BOOLEAN, + PRIMARY KEY(ID), + FOREIGN KEY('fitID') REFERENCES fits ('ID') +) +""" + +def upgrade(saveddata_engine): + saveddata_engine.execute(tmpTable) + saveddata_engine.execute("INSERT INTO boostersTemp (ID, itemID, fitID, active) SELECT ID, itemID, fitID, active FROM boosters") + saveddata_engine.execute("DROP TABLE boosters") + saveddata_engine.execute("ALTER TABLE boostersTemp RENAME TO boosters") diff --git a/eos/db/saveddata/booster.py b/eos/db/saveddata/booster.py index 459bb76b5..a31904b12 100644 --- a/eos/db/saveddata/booster.py +++ b/eos/db/saveddata/booster.py @@ -29,7 +29,7 @@ boosters_table = Table("boosters", saveddata_meta, Column("itemID", Integer), Column("fitID", Integer, ForeignKey("fits.ID"), nullable = False), Column("active", Boolean), - UniqueConstraint("itemID", "fitID")) + ) activeSideEffects_table = Table("boostersActiveSideEffects", saveddata_meta, Column("boosterID", ForeignKey("boosters.ID"), primary_key = True), diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 6dfea588b..0c5edaee9 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -27,9 +27,7 @@ from eos.db.saveddata.drone import drones_table from eos.db.saveddata.cargo import cargo_table from eos.db.saveddata.implant import fitImplants_table from eos.types import Fit, Module, User, Booster, Drone, Cargo, Implant, Character, DamagePattern, TargetResists -from eos.effectHandlerHelpers import HandledModuleList, HandledDroneList, \ -HandledImplantBoosterList, HandledProjectedModList, HandledProjectedDroneList, \ -HandledProjectedFitList, HandledCargoList +from eos.effectHandlerHelpers import * fits_table = Table("fits", saveddata_meta, Column("ID", Integer, primary_key = True), @@ -55,14 +53,16 @@ mapper(Fit, fits_table, "_Fit__projectedModules" : relation(Module, collection_class = HandledProjectedModList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)), "owner" : relation(User, backref = "fits"), + "itemID" : fits_table.c.shipID, + "shipID" : fits_table.c.shipID, "_Fit__boosters" : relation(Booster, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', single_parent=True), - "_Fit__drones" : relation(Drone, collection_class = HandledDroneList, cascade='all, delete, delete-orphan', single_parent=True, + "_Fit__drones" : relation(Drone, collection_class = HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)), - "_Fit__cargo" : relation(Cargo, collection_class = HandledCargoList, cascade='all, delete, delete-orphan', single_parent=True, + "_Fit__cargo" : relation(Cargo, collection_class = HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(cargo_table.c.fitID == fits_table.c.ID)), "_Fit__projectedDrones" : relation(Drone, collection_class = HandledProjectedDroneList, cascade='all, delete, delete-orphan', single_parent=True, primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == True)), - "_Fit__implants" : relation(Implant, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', single_parent=True, + "_Fit__implants" : relation(Implant, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', backref='fit', single_parent=True, primaryjoin = fitImplants_table.c.fitID == fits_table.c.ID, secondaryjoin = fitImplants_table.c.implantID == Implant.ID, secondary = fitImplants_table), diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index be7c078fc..b71f6b98a 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -185,6 +185,12 @@ def getFit(lookfor, eager=None): fit = saveddata_session.query(Fit).options(*eager).filter(Fit.ID == fitID).first() else: raise TypeError("Need integer as argument") + + if fit and fit.isInvalid: + with sd_lock: + removeInvalid([fit]) + return None + return fit @cachedQuery(Fleet, 1, "fleetID") @@ -244,9 +250,10 @@ def getFitsWithShip(shipID, ownerID=None, where=None, eager=None): filter = processWhere(filter, where) eager = processEager(eager) with sd_lock: - fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() + fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all()) else: raise TypeError("ShipID must be integer") + return fits def getBoosterFits(ownerID=None, where=None, eager=None): @@ -264,7 +271,8 @@ def getBoosterFits(ownerID=None, where=None, eager=None): filter = processWhere(filter, where) eager = processEager(eager) with sd_lock: - fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() + fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all()) + return fits def countAllFits(): @@ -295,7 +303,8 @@ def countFitsWithShip(shipID, ownerID=None, where=None, eager=None): def getFitList(eager=None): eager = processEager(eager) with sd_lock: - fits = saveddata_session.query(Fit).options(*eager).all() + fits = removeInvalid(saveddata_session.query(Fit).options(*eager).all()) + return fits def getFleetList(eager=None): @@ -385,7 +394,8 @@ def searchFits(nameLike, where=None, eager=None): filter = processWhere(Fit.name.like(nameLike, escape="\\"), where) eager = processEager(eager) with sd_lock: - fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() + fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all()) + return fits def getSquadsIDsWithFitID(fitID): @@ -406,6 +416,16 @@ def getProjectedFits(fitID): else: raise TypeError("Need integer as argument") +def removeInvalid(fits): + invalids = [f for f in fits if f.isInvalid] + + if invalids: + map(fits.remove, invalids) + map(saveddata_session.delete, invalids) + saveddata_session.commit() + + return fits + def add(stuff): with sd_lock: saveddata_session.add(stuff) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index b3b4c75ee..5e1997ce7 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -17,8 +17,12 @@ # along with eos. If not, see . #=============================================================================== +#from sqlalchemy.orm.attributes import flag_modified import eos.db import eos.types +import logging + +logger = logging.getLogger(__name__) class HandledList(list): def filteredItemPreAssign(self, filter, *args, **kwargs): @@ -101,6 +105,14 @@ class HandledList(list): except AttributeError: pass + def remove(self, thing): + # We must flag it as modified, otherwise it not be removed from the database + # @todo: flag_modified isn't in os x skel. need to rebuild to include + #flag_modified(thing, "itemID") + if thing.isInvalid: # see GH issue #324 + thing.itemID = 0 + list.remove(self, thing) + class HandledModuleList(HandledList): def append(self, mod): emptyPosition = float("Inf") @@ -115,10 +127,14 @@ class HandledModuleList(HandledList): del self[emptyPosition] mod.position = emptyPosition HandledList.insert(self, emptyPosition, mod) + if mod.isInvalid: + self.remove(mod) return mod.position = len(self) HandledList.append(self, mod) + if mod.isInvalid: + self.remove(mod) def insert(self, index, mod): mod.position = index @@ -149,128 +165,72 @@ class HandledModuleList(HandledList): if mod.getModifiedItemAttr("subSystemSlot") == slot: del self[i] -class HandledDroneList(HandledList): +class HandledDroneCargoList(HandledList): def find(self, item): - for d in self: - if d.item == item: - yield d + for o in self: + if o.item == item: + yield o def findFirst(self, item): - for d in self.find(item): - return d + for o in self.find(item): + return o - def append(self, drone): - list.append(self, drone) + def append(self, thing): + HandledList.append(self, thing) - def remove(self, drone): - HandledList.remove(self, drone) - - def appendItem(self, item, amount = 1): - if amount < 1: ValueError("Amount of drones to add should be >= 1") - d = self.findFirst(item) - - if d is None: - d = eos.types.Drone(item) - self.append(d) - - d.amount += amount - return d - - def removeItem(self, item, amount): - if amount < 1: ValueError("Amount of drones to remove should be >= 1") - d = self.findFirst(item) - if d is None: return - d.amount -= amount - if d.amount <= 0: - self.remove(d) - return None - - return d - -class HandledCargoList(HandledList): - # shameless copy of HandledDroneList - # I have no idea what this does, but I needed it - # @todo: investigate this - def find(self, item): - for d in self: - if d.item == item: - yield d - - def findFirst(self, item): - for d in self.find(item): - return d - - def append(self, cargo): - list.append(self, cargo) - - def remove(self, cargo): - HandledList.remove(self, cargo) - - def appendItem(self, item, qty = 1): - if qty < 1: ValueError("Amount of cargo to add should be >= 1") - d = self.findFirst(item) - - if d is None: - d = eos.types.Cargo(item) - self.append(d) - - d.qty += qty - return d - - def removeItem(self, item, qty): - if qty < 1: ValueError("Amount of cargo to remove should be >= 1") - d = self.findFirst(item) - if d is None: return - d.qty -= qty - if d.qty <= 0: - self.remove(d) - return None - - return d + if thing.isInvalid: + self.remove(thing) class HandledImplantBoosterList(HandledList): - def __init__(self): - self.__slotCache = {} + def append(self, thing): + if thing.isInvalid: + HandledList.append(self, thing) + self.remove(thing) + return - def append(self, implant): - if self.__slotCache.has_key(implant.slot): - raise ValueError("Implant/Booster slot already in use, remove the old one first or set replace = True") - self.__slotCache[implant.slot] = implant - HandledList.append(self, implant) + # if needed, remove booster that was occupying slot + oldObj = next((m for m in self if m.slot == thing.slot), None) + if oldObj: + logging.info("Slot %d occupied with %s, replacing with %s", thing.slot, oldObj.item.name, thing.item.name) + oldObj.itemID = 0 # hack to remove from DB. See GH issue #324 + self.remove(oldObj) - def remove(self, implant): - HandledList.remove(self, implant) - del self.__slotCache[implant.slot] - # While we deleted this implant, in edge case seems like not all references - # to it are removed and object still lives in session; forcibly remove it, - # or otherwise when adding the same booster twice booster's table (typeID, fitID) - # constraint will report database integrity error - # TODO: make a proper fix, probably by adjusting fit-boosters sqlalchemy relationships - eos.db.remove(implant) - - def freeSlot(self, slot): - if hasattr(slot, "slot"): - slot = slot.slot - - try: - implant = self.__slotCache[slot] - except KeyError: - return False - try: - self.remove(implant) - except ValueError: - return False - return True + HandledList.append(self, thing) class HandledProjectedModList(HandledList): + def append(self, proj): + if proj.isInvalid: + # we must include it before we remove it. doing it this way ensures + # rows and relationships in database are removed as well + HandledList.append(self, proj) + self.remove(proj) + return + + proj.projected = True + isSystemEffect = proj.item.group.name == "Effect Beacon" + + if isSystemEffect: + # remove other system effects - only 1 per fit plz + oldEffect = next((m for m in self if m.item.group.name == "Effect Beacon"), None) + + if oldEffect: + logging.info("System effect occupied with %s, replacing with %s", oldEffect.item.name, proj.item.name) + self.remove(oldEffect) + + HandledList.append(self, proj) + + # Remove non-projectable modules + if not proj.item.isType("projected") and not isSystemEffect: + self.remove(proj) + +class HandledProjectedDroneList(HandledDroneCargoList): def append(self, proj): proj.projected = True HandledList.append(self, proj) -class HandledProjectedDroneList(HandledDroneList): - def append(self, proj): - proj.projected = True - list.append(self, proj) + # Remove invalid or non-projectable drones + if proj.isInvalid or not proj.item.isType("projected"): + self.remove(proj) class HandledProjectedFitList(HandledList): def append(self, proj): diff --git a/eos/effects/ammoinfluencecapneed.py b/eos/effects/ammoinfluencecapneed.py index c695d8ff3..c9c0392f4 100644 --- a/eos/effects/ammoinfluencecapneed.py +++ b/eos/effects/ammoinfluencecapneed.py @@ -1,7 +1,7 @@ # ammoInfluenceCapNeed # # Used by: -# Items from category: Charge (458 of 829) +# Items from category: Charge (458 of 831) type = "passive" def handler(fit, module, context): # Dirty hack to work around cap charges setting cap booster diff --git a/eos/effects/ammoinfluencerange.py b/eos/effects/ammoinfluencerange.py index bb1853379..ba42613de 100644 --- a/eos/effects/ammoinfluencerange.py +++ b/eos/effects/ammoinfluencerange.py @@ -1,7 +1,7 @@ # ammoInfluenceRange # # Used by: -# Items from category: Charge (559 of 829) +# Items from category: Charge (559 of 831) type = "passive" def handler(fit, module, context): module.multiplyItemAttr("maxRange", module.getModifiedChargeAttr("weaponRangeMultiplier")) \ No newline at end of file diff --git a/eos/effects/armorhpbonusadd.py b/eos/effects/armorhpbonusadd.py index 2ea8082d2..a075cb025 100644 --- a/eos/effects/armorhpbonusadd.py +++ b/eos/effects/armorhpbonusadd.py @@ -1,7 +1,7 @@ # armorHPBonusAdd # # Used by: -# Modules from group: Armor Reinforcer (38 of 38) +# Modules from group: Armor Reinforcer (41 of 41) type = "passive" def handler(fit, module, context): fit.ship.increaseItemAttr("armorHP", module.getModifiedItemAttr("armorHPBonusAdd")) \ No newline at end of file diff --git a/eos/effects/armorreinforcermassadd.py b/eos/effects/armorreinforcermassadd.py index 4c00614b8..7a84a3f56 100644 --- a/eos/effects/armorreinforcermassadd.py +++ b/eos/effects/armorreinforcermassadd.py @@ -1,8 +1,7 @@ # armorReinforcerMassAdd # # Used by: -# Modules from group: Armor Reinforcer (38 of 38) -# Modules from group: Entosis Link (2 of 2) +# Modules from group: Armor Reinforcer (41 of 41) type = "passive" def handler(fit, module, context): fit.ship.increaseItemAttr("mass", module.getModifiedItemAttr("massAddition")) \ No newline at end of file diff --git a/eos/effects/elitebonusheavyinterdictorswarpdisruptfieldgeneratorwarpscramblerange2.py b/eos/effects/elitebonusheavyinterdictorswarpdisruptfieldgeneratorwarpscramblerange2.py index 889199e4d..9c8dc5046 100644 --- a/eos/effects/elitebonusheavyinterdictorswarpdisruptfieldgeneratorwarpscramblerange2.py +++ b/eos/effects/elitebonusheavyinterdictorswarpdisruptfieldgeneratorwarpscramblerange2.py @@ -1,7 +1,7 @@ # eliteBonusHeavyInterdictorsWarpDisruptFieldGeneratorWarpScrambleRange2 # # Used by: -# Ships from group: Heavy Interdiction Cruiser (4 of 4) +# Ships from group: Heavy Interdiction Cruiser (4 of 5) type = "passive" def handler(fit, ship, context): level = fit.character.getSkill("Heavy Interdiction Cruisers").level diff --git a/eos/effects/interceptor2warpscramblerange.py b/eos/effects/interceptor2warpscramblerange.py index d5ab4823d..8de21fae9 100644 --- a/eos/effects/interceptor2warpscramblerange.py +++ b/eos/effects/interceptor2warpscramblerange.py @@ -1,7 +1,7 @@ # Interceptor2WarpScrambleRange # # Used by: -# Ships from group: Interceptor (5 of 9) +# Ships from group: Interceptor (5 of 10) type = "passive" def handler(fit, ship, context): level = fit.character.getSkill("Interceptors").level diff --git a/eos/effects/interceptormwdsignatureradiusbonus.py b/eos/effects/interceptormwdsignatureradiusbonus.py index 82b696eae..9dedb49a4 100644 --- a/eos/effects/interceptormwdsignatureradiusbonus.py +++ b/eos/effects/interceptormwdsignatureradiusbonus.py @@ -1,7 +1,7 @@ # interceptorMWDSignatureRadiusBonus # # Used by: -# Ships from group: Interceptor (9 of 9) +# Ships from group: Interceptor (9 of 10) type = "passive" def handler(fit, ship, context): level = fit.character.getSkill("Interceptors").level diff --git a/eos/effects/missileaoecloudsizebonusonline.py b/eos/effects/missileaoecloudsizebonusonline.py new file mode 100644 index 000000000..843fc0c47 --- /dev/null +++ b/eos/effects/missileaoecloudsizebonusonline.py @@ -0,0 +1,9 @@ +# missileAOECloudSizeBonusOnline +# +# Used by: +# Modules from group: Missile Guidance Enhancer (3 of 3) +type = "passive" +def handler(fit, container, context): + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), + "aoeCloudSize", container.getModifiedItemAttr("aoeCloudSizeBonus"), + stackingPenalties=True) diff --git a/eos/effects/missileaoevelocitybonusonline.py b/eos/effects/missileaoevelocitybonusonline.py new file mode 100644 index 000000000..66eb5321b --- /dev/null +++ b/eos/effects/missileaoevelocitybonusonline.py @@ -0,0 +1,9 @@ +# missileAOEVelocityBonusOnline +# +# Used by: +# Modules from group: Missile Guidance Enhancer (3 of 3) +type = "passive" +def handler(fit, container, context): + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), + "aoeVelocity", container.getModifiedItemAttr("aoeVelocityBonus"), + stackingPenalties=True) diff --git a/eos/effects/missileexplosiondelaybonusonline.py b/eos/effects/missileexplosiondelaybonusonline.py new file mode 100644 index 000000000..5d2c770d9 --- /dev/null +++ b/eos/effects/missileexplosiondelaybonusonline.py @@ -0,0 +1,9 @@ +# missileExplosionDelayBonusOnline +# +# Used by: +# Modules from group: Missile Guidance Enhancer (3 of 3) +type = "passive" +def handler(fit, container, context): + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), + "explosionDelay", container.getModifiedItemAttr("explosionDelayBonus"), + stackingPenalties=True) diff --git a/eos/effects/missileguidancecomputerbonus4.py b/eos/effects/missileguidancecomputerbonus4.py new file mode 100644 index 000000000..d1295c657 --- /dev/null +++ b/eos/effects/missileguidancecomputerbonus4.py @@ -0,0 +1,15 @@ +# missileGuidanceComputerBonus4 +# +# Used by: +# Modules from group: Missile Guidance Computer (3 of 3) +type = "active" +def handler(fit, container, context): + for srcAttr, tgtAttr in ( + ("aoeCloudSizeBonus", "aoeCloudSize"), + ("aoeVelocityBonus", "aoeVelocity"), + ("missileVelocityBonus", "maxVelocity"), + ("explosionDelayBonus", "explosionDelay"), + ): + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), + tgtAttr, container.getModifiedItemAttr(srcAttr), + stackingPenalties=True) diff --git a/eos/effects/missileskillaoecloudsizebonus.py b/eos/effects/missileskillaoecloudsizebonus.py index fcb3ccfec..6d97e56cd 100644 --- a/eos/effects/missileskillaoecloudsizebonus.py +++ b/eos/effects/missileskillaoecloudsizebonus.py @@ -7,6 +7,7 @@ type = "passive" def handler(fit, container, context): level = container.level if "skill" in context else 1 + penalize = False if "skill" in context or "implant" in context else True fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), "aoeCloudSize", container.getModifiedItemAttr("aoeCloudSizeBonus") * level, - stackingPenalties = False) + stackingPenalties=penalize) diff --git a/eos/effects/missileskillaoevelocitybonus.py b/eos/effects/missileskillaoevelocitybonus.py index 613d97584..a059eee93 100644 --- a/eos/effects/missileskillaoevelocitybonus.py +++ b/eos/effects/missileskillaoevelocitybonus.py @@ -7,6 +7,7 @@ type = "passive" def handler(fit, container, context): level = container.level if "skill" in context else 1 + penalize = False if "skill" in context or "implant" in context else True fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), "aoeVelocity", container.getModifiedItemAttr("aoeVelocityBonus") * level, - stackingPenalties = "skill" not in context and "implant" not in context) + stackingPenalties=penalize) diff --git a/eos/effects/missilevelocitybonusdefender.py b/eos/effects/missilevelocitybonusdefender.py index 1be4804f1..3ef92fa81 100644 --- a/eos/effects/missilevelocitybonusdefender.py +++ b/eos/effects/missilevelocitybonusdefender.py @@ -5,4 +5,4 @@ type = "passive" def handler(fit, container, context): fit.modules.filteredChargeMultiply(lambda mod: mod.charge.requiresSkill("Defender Missiles"), - "maxVelocity", container.getModifiedItemAttr("missileVelocityBonus")) \ No newline at end of file + "maxVelocity", container.getModifiedItemAttr("missileVelocityBonus")) diff --git a/eos/effects/missilevelocitybonusonline.py b/eos/effects/missilevelocitybonusonline.py new file mode 100644 index 000000000..95280821a --- /dev/null +++ b/eos/effects/missilevelocitybonusonline.py @@ -0,0 +1,9 @@ +# missileVelocityBonusOnline +# +# Used by: +# Modules from group: Missile Guidance Enhancer (3 of 3) +type = "passive" +def handler(fit, container, context): + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), + "maxVelocity", container.getModifiedItemAttr("missileVelocityBonus"), + stackingPenalties=True) diff --git a/eos/effects/modeagilitypostdiv.py b/eos/effects/modeagilitypostdiv.py index 1f8ece16e..591290d5c 100644 --- a/eos/effects/modeagilitypostdiv.py +++ b/eos/effects/modeagilitypostdiv.py @@ -1,7 +1,7 @@ # modeAgilityPostDiv # # Used by: -# Modules named like: Propulsion Mode (3 of 3) +# Modules named like: Propulsion Mode (4 of 4) type = "passive" def handler(fit, module, context): fit.ship.multiplyItemAttr( diff --git a/eos/effects/modearmorrepdurationpostdiv.py b/eos/effects/modearmorrepdurationpostdiv.py new file mode 100644 index 000000000..ecfc76c1f --- /dev/null +++ b/eos/effects/modearmorrepdurationpostdiv.py @@ -0,0 +1,11 @@ +# modeArmorRepDurationPostDiv +# +# Used by: +# Module: Hecate Defense Mode +type = "passive" +def handler(fit, module, context): + fit.modules.filteredItemMultiply( + lambda mod: mod.item.requiresSkill("Repair Systems"), + "duration", + 1 / module.getModifiedItemAttr("modeArmorRepDurationPostDiv") + ) diff --git a/eos/effects/modearmorresonancepostdiv.py b/eos/effects/modearmorresonancepostdiv.py index 27121df39..708fafc9e 100644 --- a/eos/effects/modearmorresonancepostdiv.py +++ b/eos/effects/modearmorresonancepostdiv.py @@ -1,8 +1,7 @@ # modeArmorResonancePostDiv # # Used by: -# Module: Confessor Defense Mode -# Module: Svipul Defense Mode +# Modules named like: Defense Mode (3 of 4) type = "passive" def handler(fit, module, context): for srcResType, tgtResType in ( diff --git a/eos/effects/modehullresonancepostdiv.py b/eos/effects/modehullresonancepostdiv.py new file mode 100644 index 000000000..bc38b1e18 --- /dev/null +++ b/eos/effects/modehullresonancepostdiv.py @@ -0,0 +1,16 @@ +# modeHullResonancePostDiv +# +# Used by: +# Module: Hecate Defense Mode +type = "passive" +def handler(fit, module, context): + for srcResType, tgtResType in ( + ("Em", "em"), + ("Explosive", "explosive"), + ("Kinetic", "kinetic"), + ("Thermic", "thermal") + ): + fit.ship.multiplyItemAttr( + "{0}DamageResonance".format(tgtResType), + 1 / module.getModifiedItemAttr("mode{0}ResistancePostDiv".format(srcResType)) + ) diff --git a/eos/effects/modemwdboostpostdiv.py b/eos/effects/modemwdboostpostdiv.py new file mode 100644 index 000000000..5669e906b --- /dev/null +++ b/eos/effects/modemwdboostpostdiv.py @@ -0,0 +1,13 @@ +# modeMWDBoostPostDiv +# +# Used by: +# Module: Hecate Propulsion Mode +type = "passive" +def handler(fit, module, context): + fit.modules.filteredItemMultiply( + lambda mod: mod.item.requiresSkill("High Speed Maneuvering"), + "speedFactor", + 1 / module.getModifiedItemAttr("modeMWDVelocityPostDiv"), + stackingPenalties=True, + penaltyGroup="postDiv" + ) diff --git a/eos/effects/modemwdcappostdiv.py b/eos/effects/modemwdcappostdiv.py new file mode 100644 index 000000000..7ad0ba151 --- /dev/null +++ b/eos/effects/modemwdcappostdiv.py @@ -0,0 +1,11 @@ +# modeMWDCapPostDiv +# +# Used by: +# Module: Hecate Propulsion Mode +type = "passive" +def handler(fit, module, context): + fit.modules.filteredItemMultiply( + lambda mod: mod.item.requiresSkill("High Speed Maneuvering"), + "capacitorNeed", + 1 / module.getModifiedItemAttr("modeMWDCapPostDiv") + ) diff --git a/eos/effects/modevelocitypostdiv.py b/eos/effects/modevelocitypostdiv.py index f3b1090ab..a781893c6 100644 --- a/eos/effects/modevelocitypostdiv.py +++ b/eos/effects/modevelocitypostdiv.py @@ -1,7 +1,7 @@ # modeVelocityPostDiv # # Used by: -# Modules named like: Propulsion Mode (3 of 3) +# Modules named like: Propulsion Mode (3 of 4) type = "passive" def handler(fit, module, context): fit.ship.multiplyItemAttr( diff --git a/eos/effects/offensivedefensivereduction.py b/eos/effects/offensivedefensivereduction.py index 70e98cca9..cc6abc5f5 100644 --- a/eos/effects/offensivedefensivereduction.py +++ b/eos/effects/offensivedefensivereduction.py @@ -1,6 +1,7 @@ # OffensiveDefensiveReduction # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Incursion ship attributes effects (3 of 3) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/overloadselfmissileguidancebonus5.py b/eos/effects/overloadselfmissileguidancebonus5.py new file mode 100644 index 000000000..0f99c4965 --- /dev/null +++ b/eos/effects/overloadselfmissileguidancebonus5.py @@ -0,0 +1,14 @@ +# overloadSelfMissileGuidanceBonus5 +# +# Used by: +# Modules from group: Missile Guidance Computer (3 of 3) +type = "overheat" +def handler(fit, module, context): + for tgtAttr in ( + "aoeCloudSizeBonus", + "explosionDelayBonus", + "missileVelocityBonus", + "maxVelocityBonus", + "aoeVelocityBonus" + ): + module.boostItemAttr(tgtAttr, module.getModifiedItemAttr("overloadTrackingModuleStrengthBonus")) diff --git a/eos/effects/passivemassadd.py b/eos/effects/passivemassadd.py new file mode 100644 index 000000000..7d33d29ed --- /dev/null +++ b/eos/effects/passivemassadd.py @@ -0,0 +1,7 @@ +# passiveMassAdd +# +# Used by: +# Modules from group: Entosis Link (2 of 2) +type = "offline" +def handler(fit, module, context): + fit.ship.increaseItemAttr("mass", module.getModifiedItemAttr("massAddition")) diff --git a/eos/effects/probelaunchercpupercentbonustacticaldestroyer.py b/eos/effects/probelaunchercpupercentbonustacticaldestroyer.py index 05cd1f11c..99cbeb9b7 100644 --- a/eos/effects/probelaunchercpupercentbonustacticaldestroyer.py +++ b/eos/effects/probelaunchercpupercentbonustacticaldestroyer.py @@ -1,7 +1,7 @@ # probeLauncherCPUPercentBonusTacticalDestroyer # # Used by: -# Ships from group: Tactical Destroyer (3 of 3) +# Ships from group: Tactical Destroyer (4 of 4) type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Astrometrics"), diff --git a/eos/effects/scriptmissileguidancecomputeraoecloudsizebonusbonus.py b/eos/effects/scriptmissileguidancecomputeraoecloudsizebonusbonus.py new file mode 100644 index 000000000..d83cdc907 --- /dev/null +++ b/eos/effects/scriptmissileguidancecomputeraoecloudsizebonusbonus.py @@ -0,0 +1,7 @@ +# scriptMissileGuidanceComputerAOECloudSizeBonusBonus +# +# Used by: +# Charges from group: Missile Guidance Script (2 of 2) +type = "passive" +def handler(fit, module, context): + module.boostItemAttr("aoeCloudSizeBonus", module.getModifiedChargeAttr("aoeCloudSizeBonusBonus")) diff --git a/eos/effects/scriptmissileguidancecomputeraoevelocitybonusbonus.py b/eos/effects/scriptmissileguidancecomputeraoevelocitybonusbonus.py new file mode 100644 index 000000000..8a0f956ea --- /dev/null +++ b/eos/effects/scriptmissileguidancecomputeraoevelocitybonusbonus.py @@ -0,0 +1,7 @@ +# scriptMissileGuidanceComputerAOEVelocityBonusBonus +# +# Used by: +# Charges from group: Missile Guidance Script (2 of 2) +type = "passive" +def handler(fit, module, context): + module.boostItemAttr("aoeVelocityBonus", module.getModifiedChargeAttr("aoeVelocityBonusBonus")) diff --git a/eos/effects/scriptmissileguidancecomputerexplosiondelaybonusbonus.py b/eos/effects/scriptmissileguidancecomputerexplosiondelaybonusbonus.py new file mode 100644 index 000000000..186bbc635 --- /dev/null +++ b/eos/effects/scriptmissileguidancecomputerexplosiondelaybonusbonus.py @@ -0,0 +1,7 @@ +# scriptMissileGuidanceComputerExplosionDelayBonusBonus +# +# Used by: +# Charges from group: Missile Guidance Script (2 of 2) +type = "passive" +def handler(fit, module, context): + module.boostItemAttr("explosionDelayBonus", module.getModifiedChargeAttr("explosionDelayBonusBonus")) diff --git a/eos/effects/scriptmissileguidancecomputermissilevelocitybonusbonus.py b/eos/effects/scriptmissileguidancecomputermissilevelocitybonusbonus.py new file mode 100644 index 000000000..8ab36f8a8 --- /dev/null +++ b/eos/effects/scriptmissileguidancecomputermissilevelocitybonusbonus.py @@ -0,0 +1,7 @@ +# scriptMissileGuidanceComputerMissileVelocityBonusBonus +# +# Used by: +# Charges from group: Missile Guidance Script (2 of 2) +type = "passive" +def handler(fit, module, context): + module.boostItemAttr("missileVelocityBonus", module.getModifiedChargeAttr("missileVelocityBonusBonus")) diff --git a/eos/effects/shipbonuspiratesmallhybriddmg.py b/eos/effects/shipbonuspiratesmallhybriddmg.py index 6ddeae4e6..986b5d59b 100644 --- a/eos/effects/shipbonuspiratesmallhybriddmg.py +++ b/eos/effects/shipbonuspiratesmallhybriddmg.py @@ -2,6 +2,7 @@ # # Used by: # Ship: Daredevil +# Ship: Hecate type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Hybrid Turret"), diff --git a/eos/effects/shipbonusrhmlrofcb.py b/eos/effects/shipbonusrhmlrofcb.py index c1a1f4c5f..058aa2b9e 100644 --- a/eos/effects/shipbonusrhmlrofcb.py +++ b/eos/effects/shipbonusrhmlrofcb.py @@ -1,3 +1,7 @@ +# shipBonusRHMLROFCB +# +# Used by: +# Ship: Scorpion Navy Issue type = "passive" def handler(fit, ship, context): level = fit.character.getSkill("Caldari Battleship").level diff --git a/eos/effects/shipcappropulsionjamming.py b/eos/effects/shipcappropulsionjamming.py index 8523593ca..9f7e60a39 100644 --- a/eos/effects/shipcappropulsionjamming.py +++ b/eos/effects/shipcappropulsionjamming.py @@ -1,7 +1,7 @@ # shipCapPropulsionJamming # # Used by: -# Ships from group: Interceptor (9 of 9) +# Ships from group: Interceptor (9 of 10) # Ship: Atron # Ship: Condor # Ship: Executioner diff --git a/eos/effects/shipheatdamagegallentetacticaldestroyer3.py b/eos/effects/shipheatdamagegallentetacticaldestroyer3.py new file mode 100644 index 000000000..9d09f8dc6 --- /dev/null +++ b/eos/effects/shipheatdamagegallentetacticaldestroyer3.py @@ -0,0 +1,9 @@ +# shipHeatDamageGallenteTacticalDestroyer3 +# +# Used by: +# Ship: Hecate +type = "passive" +def handler(fit, ship, context): + level = fit.character.getSkill("Gallente Tactical Destroyer").level + fit.modules.filteredItemBoost(lambda mod: True, "heatDamage", + ship.getModifiedItemAttr("shipBonusTacticalDestroyerGallente3") * level) diff --git a/eos/effects/shipmodemaxtargetrangepostdiv.py b/eos/effects/shipmodemaxtargetrangepostdiv.py index 4e160e1cc..e38ed8dfe 100644 --- a/eos/effects/shipmodemaxtargetrangepostdiv.py +++ b/eos/effects/shipmodemaxtargetrangepostdiv.py @@ -1,7 +1,7 @@ # shipModeMaxTargetRangePostDiv # # Used by: -# Modules named like: Sharpshooter Mode (3 of 3) +# Modules named like: Sharpshooter Mode (4 of 4) type = "passive" def handler(fit, module, context): fit.ship.multiplyItemAttr( diff --git a/eos/effects/shipmodescanrespostdiv.py b/eos/effects/shipmodescanrespostdiv.py index d7cc9b72a..f89c8152f 100644 --- a/eos/effects/shipmodescanrespostdiv.py +++ b/eos/effects/shipmodescanrespostdiv.py @@ -1,7 +1,7 @@ # shipModeScanResPostDiv # # Used by: -# Modules named like: Sharpshooter Mode (3 of 3) +# Modules named like: Sharpshooter Mode (4 of 4) type = "passive" def handler(fit, module, context): fit.ship.multiplyItemAttr( diff --git a/eos/effects/shipmodescanstrengthpostdiv.py b/eos/effects/shipmodescanstrengthpostdiv.py index 7e4613108..eac3f8b61 100644 --- a/eos/effects/shipmodescanstrengthpostdiv.py +++ b/eos/effects/shipmodescanstrengthpostdiv.py @@ -1,7 +1,7 @@ # shipModeScanStrengthPostDiv # # Used by: -# Modules named like: Sharpshooter Mode (3 of 3) +# Modules named like: Sharpshooter Mode (4 of 4) type = "passive" def handler(fit, module, context): for scanType in ("Gravimetric", "Magnetometric", "Radar", "Ladar"): diff --git a/eos/effects/shipmodeshtoptimalrangepostdiv.py b/eos/effects/shipmodeshtoptimalrangepostdiv.py new file mode 100644 index 000000000..4b38fdd71 --- /dev/null +++ b/eos/effects/shipmodeshtoptimalrangepostdiv.py @@ -0,0 +1,13 @@ +# shipModeSHTOptimalRangePostDiv +# +# Used by: +# Module: Hecate Sharpshooter Mode +type = "passive" +def handler(fit, module, context): + fit.modules.filteredItemMultiply( + lambda mod: mod.item.requiresSkill("Small Hybrid Turret"), + "maxRange", + 1 / module.getModifiedItemAttr("modeMaxRangePostDiv"), + stackingPenalties=True, + penaltyGroup="postDiv" + ) diff --git a/eos/effects/shipshtrofgallentetacticaldestroyer1.py b/eos/effects/shipshtrofgallentetacticaldestroyer1.py new file mode 100644 index 000000000..3d288c289 --- /dev/null +++ b/eos/effects/shipshtrofgallentetacticaldestroyer1.py @@ -0,0 +1,9 @@ +# shipSHTRoFGallenteTacticalDestroyer1 +# +# Used by: +# Ship: Hecate +type = "passive" +def handler(fit, ship, context): + level = fit.character.getSkill("Gallente Tactical Destroyer").level + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Hybrid Turret"), + "speed", ship.getModifiedItemAttr("shipBonusTacticalDestroyerGallente1") * level) diff --git a/eos/effects/shipshttrackinggallentetacticaldestroyer2.py b/eos/effects/shipshttrackinggallentetacticaldestroyer2.py new file mode 100644 index 000000000..a2d6a612c --- /dev/null +++ b/eos/effects/shipshttrackinggallentetacticaldestroyer2.py @@ -0,0 +1,9 @@ +# shipSHTTrackingGallenteTacticalDestroyer2 +# +# Used by: +# Ship: Hecate +type = "passive" +def handler(fit, ship, context): + level = fit.character.getSkill("Gallente Tactical Destroyer").level + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Hybrid Turret"), + "trackingSpeed", ship.getModifiedItemAttr("shipBonusTacticalDestroyerGallente2") * level) diff --git a/eos/effects/systemdamagedrones.py b/eos/effects/systemdamagedrones.py index 889c75378..c887cc51f 100644 --- a/eos/effects/systemdamagedrones.py +++ b/eos/effects/systemdamagedrones.py @@ -1,6 +1,7 @@ # systemDamageDrones # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/systemdamageemmissiles.py b/eos/effects/systemdamageemmissiles.py index eb12ddce3..61e1c5b7c 100644 --- a/eos/effects/systemdamageemmissiles.py +++ b/eos/effects/systemdamageemmissiles.py @@ -1,6 +1,7 @@ # systemDamageEmMissiles # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/systemdamageexplosivemissiles.py b/eos/effects/systemdamageexplosivemissiles.py index ade40917f..85eec6530 100644 --- a/eos/effects/systemdamageexplosivemissiles.py +++ b/eos/effects/systemdamageexplosivemissiles.py @@ -1,6 +1,7 @@ # systemDamageExplosiveMissiles # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/systemdamagekineticmissiles.py b/eos/effects/systemdamagekineticmissiles.py index cb35e4613..b36d47690 100644 --- a/eos/effects/systemdamagekineticmissiles.py +++ b/eos/effects/systemdamagekineticmissiles.py @@ -1,6 +1,7 @@ # systemDamageKineticMissiles # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/systemdamagemultipliergunnery.py b/eos/effects/systemdamagemultipliergunnery.py index d940a0b73..70849fa79 100644 --- a/eos/effects/systemdamagemultipliergunnery.py +++ b/eos/effects/systemdamagemultipliergunnery.py @@ -1,6 +1,7 @@ # systemDamageMultiplierGunnery # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/systemdamagethermalmissiles.py b/eos/effects/systemdamagethermalmissiles.py index 2738832a7..3dee7a4c8 100644 --- a/eos/effects/systemdamagethermalmissiles.py +++ b/eos/effects/systemdamagethermalmissiles.py @@ -1,6 +1,7 @@ # systemDamageThermalMissiles # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/systemmaxvelocity.py b/eos/effects/systemmaxvelocity.py index 9f3e78ec3..13c3bdd7f 100644 --- a/eos/effects/systemmaxvelocity.py +++ b/eos/effects/systemmaxvelocity.py @@ -2,6 +2,7 @@ # # Used by: # Celestials named like: Black Hole Effect Beacon Class (6 of 6) +# Celestials named like: Drifter Incursion (6 of 6) runTime = "early" type = ("projected", "offline") def handler(fit, beacon, context): diff --git a/eos/effects/systemsmartbombemdamage.py b/eos/effects/systemsmartbombemdamage.py index 20f06d156..44964f681 100644 --- a/eos/effects/systemsmartbombemdamage.py +++ b/eos/effects/systemsmartbombemdamage.py @@ -1,6 +1,7 @@ # systemSmartBombEmDamage # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Red Giant Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/systemsmartbombexplosivedamage.py b/eos/effects/systemsmartbombexplosivedamage.py index 40e6ffde9..e27857d85 100644 --- a/eos/effects/systemsmartbombexplosivedamage.py +++ b/eos/effects/systemsmartbombexplosivedamage.py @@ -1,6 +1,7 @@ # systemSmartBombExplosiveDamage # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Red Giant Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/systemsmartbombkineticdamage.py b/eos/effects/systemsmartbombkineticdamage.py index 41d317e4d..0e8d9730b 100644 --- a/eos/effects/systemsmartbombkineticdamage.py +++ b/eos/effects/systemsmartbombkineticdamage.py @@ -1,6 +1,7 @@ # systemSmartBombKineticDamage # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Red Giant Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/effects/systemsmartbombthermaldamage.py b/eos/effects/systemsmartbombthermaldamage.py index 0237c98f4..c5ae52efb 100644 --- a/eos/effects/systemsmartbombthermaldamage.py +++ b/eos/effects/systemsmartbombthermaldamage.py @@ -1,6 +1,7 @@ # systemSmartBombThermalDamage # # Used by: +# Celestials named like: Drifter Incursion (6 of 6) # Celestials named like: Red Giant Beacon Class (6 of 6) runTime = "early" type = ("projected", "offline") diff --git a/eos/saveddata/booster.py b/eos/saveddata/booster.py index 001a163de..c0874fe94 100644 --- a/eos/saveddata/booster.py +++ b/eos/saveddata/booster.py @@ -20,36 +20,53 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem from sqlalchemy.orm import reconstructor, validates +import eos.db +import logging + +logger = logging.getLogger(__name__) class Booster(HandledItem, ItemAttrShortcut): def __init__(self, item): - self.__slot = self.__calculateSlot(item) - self.itemID = item.ID self.__item = item + + if self.isInvalid: + raise ValueError("Passed item is not a Booster") + + self.itemID = item.ID if item is not None else None self.active = True self.build() @reconstructor def init(self): + """Initialize a booster from the database and validate""" self.__item = None + if self.itemID: + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return + + if self.isInvalid: + logger.error("Item (id: %d) is not a Booser", self.itemID) + return + + self.build() + def build(self): + """ Build object. Assumes proper and valid item already set """ + self.__sideEffects = [] self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes.original = self.__item.attributes - self.__sideEffects = [] - for effect in self.item.effects.itervalues(): + self.__slot = self.__calculateSlot(self.__item) + + for effect in self.__item.effects.itervalues(): if effect.isType("boosterSideEffect"): s = SideEffect(self) s.effect = effect s.active = effect.ID in self.__activeSideEffectIDs self.__sideEffects.append(s) - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.itemID) - self.__slot = self.__calculateSlot(self.__item) - self.build() - def iterSideEffects(self): return self.__sideEffects.__iter__() @@ -62,23 +79,18 @@ class Booster(HandledItem, ItemAttrShortcut): @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property - def slot(self): - if self.__item is None: - self.__fetchItemInfo() + def isInvalid(self): + return self.__item is None or self.__item.group.name != "Booster" + @property + def slot(self): return self.__slot @property def item(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__item def __calculateSlot(self, item): diff --git a/eos/saveddata/cargo.py b/eos/saveddata/cargo.py index 95ef072b6..26509d525 100644 --- a/eos/saveddata/cargo.py +++ b/eos/saveddata/cargo.py @@ -20,40 +20,45 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.effectHandlerHelpers import HandledItem, HandledCharge from sqlalchemy.orm import validates, reconstructor +import eos.db +import logging -# Cargo class copied from Implant class and hacked to make work. \o/ -# @todo: clean me up, Scotty -class Cargo(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): +logger = logging.getLogger(__name__) + +class Cargo(HandledItem, ItemAttrShortcut): def __init__(self, item): + """Initialize cargo from the program""" self.__item = item - self.itemID = item.ID + self.itemID = item.ID if item is not None else None self.amount = 0 self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.item.attributes + self.__itemModifiedAttributes.original = item.attributes @reconstructor def init(self): + """Initialize cargo from the database and validate""" self.__item = None - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.itemID) + if self.itemID: + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return + self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes.original = self.__item.attributes @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property - def item(self): - if self.__item is None: - self.__fetchItemInfo() + def isInvalid(self): + return self.__item is None + @property + def item(self): return self.__item def clear(self): diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index 289674f1e..1a63d57a3 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -20,81 +20,80 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.effectHandlerHelpers import HandledItem, HandledCharge from sqlalchemy.orm import validates, reconstructor +import eos.db +import logging + +logger = logging.getLogger(__name__) class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal") MINING_ATTRIBUTES = ("miningAmount",) def __init__(self, item): - if item.category.name != "Drone": - raise ValueError("Passed item is not a drone") - + """Initialize a drone from the program""" self.__item = item - self.__charge = None - self.itemID = item.ID + + if self.isInvalid: + raise ValueError("Passed item is not a Drone") + + self.itemID = item.ID if item is not None else None self.amount = 0 self.amountActive = 0 - self.__dps = None - self.__volley = None - self.__miningyield = None self.projected = False - self.__itemModifiedAttributes = ModifiedAttributeDict() - self.itemModifiedAttributes.original = self.item.attributes + self.build() @reconstructor def init(self): + """Initialize a drone from the database and validate""" + self.__item = None + + if self.itemID: + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return + + if self.isInvalid: + logger.error("Item (id: %d) is not a Drone", self.itemID) + return + + self.build() + + def build(self): + """ Build object. Assumes proper and valid item already set """ + self.__charge = None self.__dps = None self.__volley = None self.__miningyield = None - self.__item = None - self.__charge = None - - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.itemID) - self.__charge = None self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.item.attributes + self.__itemModifiedAttributes.original = self.__item.attributes - def __fetchChargeInfo(self): - chargeID = self.getModifiedItemAttr("entityMissileTypeID") self.__chargeModifiedAttributes = ModifiedAttributeDict() + chargeID = self.getModifiedItemAttr("entityMissileTypeID") if chargeID is not None: - import eos.db charge = eos.db.getItem(int(chargeID)) self.__charge = charge - - self.chargeModifiedAttributes.original = charge.attributes - else: - self.__charge = 0 + self.__chargeModifiedAttributes.original = charge.attributes @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property def chargeModifiedAttributes(self): - if self.__charge is None: - self.__fetchChargeInfo() - return self.__chargeModifiedAttributes @property - def item(self): - if self.__item is None: - self.__fetchItemInfo() + def isInvalid(self): + return self.__item is None or self.__item.category.name != "Drone" + @property + def item(self): return self.__item @property def charge(self): - if self.__charge is None: - self.__fetchChargeInfo() - - return self.__charge if self.__charge != 0 else None + return self.__charge @property def dealsDamage(self): diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index f8328371f..5a277070c 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -17,8 +17,7 @@ # along with eos. If not, see . #=============================================================================== -from eos.effectHandlerHelpers import HandledList, HandledModuleList, HandledDroneList, HandledImplantBoosterList, \ -HandledProjectedFitList, HandledProjectedModList, HandledProjectedDroneList, HandledCargoList +from eos.effectHandlerHelpers import * from eos.modifiedAttributeDict import ModifiedAttributeDict from sqlalchemy.orm import validates, reconstructor from itertools import chain @@ -28,7 +27,11 @@ from math import sqrt, log, asinh from eos.types import Drone, Cargo, Ship, Character, State, Slot, Module, Implant, Booster, Skill from eos.saveddata.module import State from eos.saveddata.mode import Mode +import eos.db import time +import logging + +logger = logging.getLogger(__name__) try: from collections import OrderedDict @@ -48,10 +51,14 @@ class Fit(object): PEAK_RECHARGE = 0.25 - def __init__(self): + def __init__(self, ship=None, name=""): + """Initialize a fit from the program""" + # use @mode.setter's to set __attr and IDs. This will set mode as well + self.ship = ship + self.__modules = HandledModuleList() - self.__drones = HandledDroneList() - self.__cargo = HandledCargoList() + self.__drones = HandledDroneCargoList() + self.__cargo = HandledDroneCargoList() self.__implants = HandledImplantBoosterList() self.__boosters = HandledImplantBoosterList() self.__projectedFits = HandledProjectedFitList() @@ -59,23 +66,42 @@ class Fit(object): self.__projectedDrones = HandledProjectedDroneList() self.__character = None self.__owner = None - self.shipID = None + self.projected = False - self.name = "" - self.fleet = None - self.boostsFits = set() - self.gangBoosts = None + self.name = name self.timestamp = time.time() - self.ecmProjectedStr = 1 self.modeID = None + self.build() @reconstructor def init(self): + """Initialize a fit from the database and validate""" + self.__ship = None + self.__mode = None + + if self.shipID: + item = eos.db.getItem(self.shipID) + if item is None: + logger.error("Item (id: %d) does not exist", self.shipID) + return + + try: + self.__ship = Ship(item) + except ValueError: + logger.error("Item (id: %d) is not a Ship", self.shipID) + return + + if self.modeID and self.__ship: + item = eos.db.getItem(self.modeID) + # Don't need to verify if it's a proper item, as validateModeItem assures this + self.__mode = self.ship.validateModeItem(item) + else: + self.__mode = self.ship.validateModeItem(None) + self.build() def build(self): - from eos import db self.__extraDrains = [] self.__ehp = None self.__weaponDPS = None @@ -100,11 +126,6 @@ class Fit(object): self.ecmProjectedStr = 1 self.extraAttributes = ModifiedAttributeDict(self) self.extraAttributes.original = self.EXTRA_ATTRIBUTES - self.ship = Ship(db.getItem(self.shipID)) if self.shipID is not None else None - if self.ship is not None: - self.mode = self.ship.checkModeItem(db.getItem(self.modeID) if self.modeID else None) - else: - self.mode = None @property def targetResists(self): @@ -128,13 +149,17 @@ class Fit(object): self.__ehp = None self.__effectiveTank = None + @property + def isInvalid(self): + return self.__ship is None + @property def mode(self): - return self._mode + return self.__mode @mode.setter def mode(self, mode): - self._mode = mode + self.__mode = mode self.modeID = mode.item.ID if mode is not None else None @property @@ -154,7 +179,7 @@ class Fit(object): self.__ship = ship self.shipID = ship.item.ID if ship is not None else None # set mode of new ship - self.mode = self.ship.checkModeItem(None) if ship is not None else None + self.mode = self.ship.validateModeItem(None) if ship is not None else None @property def drones(self): diff --git a/eos/saveddata/implant.py b/eos/saveddata/implant.py index 036eabbc3..64670d769 100644 --- a/eos/saveddata/implant.py +++ b/eos/saveddata/implant.py @@ -20,46 +20,58 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem from sqlalchemy.orm import validates, reconstructor +import eos.db +import logging + +logger = logging.getLogger(__name__) class Implant(HandledItem, ItemAttrShortcut): def __init__(self, item): - self.__slot = self.__calculateSlot(item) self.__item = item - self.itemID = item.ID + + if self.isInvalid: + raise ValueError("Passed item is not an Implant") + + self.itemID = item.ID if item is not None else None self.active = True - self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.item.attributes + self.build() @reconstructor def init(self): self.__item = None - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.itemID) + if self.itemID: + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return + + if self.isInvalid: + logger.error("Item (id: %d) is not an Implant", self.itemID) + return + + self.build() + + def build(self): + """ Build object. Assumes proper and valid item already set """ self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes.original = self.__item.attributes self.__slot = self.__calculateSlot(self.__item) @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property - def slot(self): - if self.__item is None: - self.__fetchItemInfo() + def isInvalid(self): + return self.__item is None or self.__item.category.name != "Implant" + @property + def slot(self): return self.__slot @property def item(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__item def __calculateSlot(self, item): diff --git a/eos/saveddata/mode.py b/eos/saveddata/mode.py index 83d0c0bc2..3a344d74d 100644 --- a/eos/saveddata/mode.py +++ b/eos/saveddata/mode.py @@ -19,36 +19,24 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem +import eos.db class Mode(ItemAttrShortcut, HandledItem): - def __init__(self, item): + + if item.group.name != "Ship Modifiers": + raise ValueError('Passed item "%s" (category: (%s)) is not a Ship Modifier'%(item.name, item.category.name)) + self.__item = item self.__itemModifiedAttributes = ModifiedAttributeDict() - - if not isinstance(item, int): - self.__buildOriginal() - - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.__item) - self.__buildOriginal() - - def __buildOriginal(self): self.__itemModifiedAttributes.original = self.item.attributes @property def item(self): - if isinstance(self.__item, int): - self.__fetchItemInfo() - return self.__item @property def itemModifiedAttributes(self): - if isinstance(self.__item, int): - self.__fetchItemInfo() - return self.__itemModifiedAttributes # @todo: rework to fit only on t3 dessy diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 4688e53c4..a4fdbec07 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -23,6 +23,10 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, C from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.enum import Enum from eos.mathUtils import floorFloat +import eos.db +import logging + +logger = logging.getLogger(__name__) class State(Enum): OFFLINE = -1 @@ -49,95 +53,78 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): MINING_ATTRIBUTES = ("miningAmount", ) def __init__(self, item): - self.__item = item if item != None else 0 + """Initialize a module from the program""" + self.__item = item + + if item is not None and self.isInvalid: + raise ValueError("Passed item is not a Module") + + self.__charge = None self.itemID = item.ID if item is not None else None - self.__charge = 0 self.projected = False self.state = State.ONLINE + self.build() + + @reconstructor + def init(self): + """Initialize a module from the database and validate""" + self.__item = None + self.__charge = None + + # we need this early if module is invalid and returns early + self.__slot = self.dummySlot + + if self.itemID: + self.__item = eos.db.getItem(self.itemID) + if self.__item is None: + logger.error("Item (id: %d) does not exist", self.itemID) + return + + if self.isInvalid: + logger.error("Item (id: %d) is not a Module", self.itemID) + return + + if self.chargeID: + self.__charge = eos.db.getItem(self.chargeID) + + self.build() + + def build(self): + """ Builds internal module variables from both init's """ + + if self.__charge and self.__charge.category.name != "Charge": + self.__charge = None + self.__dps = None self.__miningyield = None self.__volley = None self.__reloadTime = None self.__reloadForce = None self.__chargeCycles = None + self.__hardpoint = Hardpoint.NONE self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__slot = None - - if item != None: - self.__itemModifiedAttributes.original = item.attributes - self.__hardpoint = self.__calculateHardpoint(item) - self.__slot = self.__calculateSlot(item) - self.__chargeModifiedAttributes = ModifiedAttributeDict() + self.__slot = self.dummySlot # defaults to None - @reconstructor - def init(self): - if self.dummySlot is None: - self.__item = None - self.__charge = None - self.__volley = None - self.__dps = None - self.__miningyield = None - self.__reloadTime = None - self.__reloadForce = None - self.__chargeCycles = None - else: - self.__slot = self.dummySlot - self.__item = 0 - self.__charge = 0 - self.__dps = 0 - self.__miningyield = 0 - self.__volley = 0 - self.__reloadTime = 0 - self.__reloadForce = None - self.__chargeCycles = 0 - self.__hardpoint = Hardpoint.NONE - self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__chargeModifiedAttributes = ModifiedAttributeDict() - - def __fetchItemInfo(self): - import eos.db - item = eos.db.getItem(self.itemID) - self.__item = item - self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = item.attributes - self.__hardpoint = self.__calculateHardpoint(item) - self.__slot = self.__calculateSlot(item) - - def __fetchChargeInfo(self): - self.__chargeModifiedAttributes = ModifiedAttributeDict() - if self.chargeID is not None: - import eos.db - charge = eos.db.getItem(self.chargeID) - self.__charge = charge - self.__chargeModifiedAttributes.original = charge.attributes - else: - self.__charge = 0 + if self.__item: + self.__itemModifiedAttributes.original = self.__item.attributes + self.__hardpoint = self.__calculateHardpoint(self.__item) + self.__slot = self.__calculateSlot(self.__item) + if self.__charge: + self.__chargeModifiedAttributes.original = self.__charge.attributes @classmethod def buildEmpty(cls, slot): empty = Module(None) empty.__slot = slot - empty.__hardpoint = Hardpoint.NONE - empty.__item = 0 - empty.__charge = 0 empty.dummySlot = slot - empty.__itemModifiedAttributes = ModifiedAttributeDict() - empty.__chargeModifiedAttributes = ModifiedAttributeDict() - return empty @classmethod def buildRack(cls, slot): empty = Rack(None) empty.__slot = slot - empty.__hardpoint = Hardpoint.NONE - empty.__item = 0 - empty.__charge = 0 empty.dummySlot = slot - empty.__itemModifiedAttributes = ModifiedAttributeDict() - empty.__chargeModifiedAttributes = ModifiedAttributeDict() - return empty @property @@ -146,11 +133,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def hardpoint(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__hardpoint + @property + def isInvalid(self): + if self.isEmpty: + return False + return self.__item is None or (self.__item.category.name not in ("Module", "Subsystem") and self.__item.group.name != "Effect Beacon") + @property def numCharges(self): if self.charge is None: @@ -263,38 +253,23 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @property def slot(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__slot @property def itemModifiedAttributes(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__itemModifiedAttributes @property def chargeModifiedAttributes(self): - if self.__charge is None: - self.__fetchChargeInfo() - return self.__chargeModifiedAttributes @property def item(self): - if self.__item is None: - self.__fetchItemInfo() - return self.__item if self.__item != 0 else None @property def charge(self): - if self.__charge is None: - self.__fetchChargeInfo() - return self.__charge if self.__charge != 0 else None @charge.setter @@ -516,7 +491,6 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def getValidCharges(self): validCharges = set() - import eos.db for i in range(5): itemChargeGroup = self.getModifiedItemAttr('chargeGroup' + str(i)) if itemChargeGroup is not None: diff --git a/eos/saveddata/price.py b/eos/saveddata/price.py index 4c7de879c..ea9b0ac92 100644 --- a/eos/saveddata/price.py +++ b/eos/saveddata/price.py @@ -26,6 +26,7 @@ class Price(object): def __init__(self, typeID): self.typeID = typeID self.time = 0 + self.price = 0 self.failed = None self.__item = None diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index 03eeef3d4..86123bfc1 100644 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -20,6 +20,10 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.effectHandlerHelpers import HandledItem from eos.saveddata.mode import Mode +import eos.db +import logging + +logger = logging.getLogger(__name__) class Ship(ItemAttrShortcut, HandledItem): def __init__(self, item): @@ -28,33 +32,18 @@ class Ship(ItemAttrShortcut, HandledItem): raise ValueError('Passed item "%s" (category: (%s)) is not under Ship category'%(item.name, item.category.name)) self.__item = item + self.__modeItems = self.__getModeItems() self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__modeItems = self._getModeItems() - if not isinstance(item, int): - self.__buildOriginal() + self.__itemModifiedAttributes.original = self.item.attributes self.commandBonus = 0 - def __fetchItemInfo(self): - import eos.db - self.__item = eos.db.getItem(self.__item) - self.__buildOriginal() - - def __buildOriginal(self): - self.__itemModifiedAttributes.original = self.item.attributes - @property def item(self): - if isinstance(self.__item, int): - self.__fetchItemInfo() - return self.__item @property def itemModifiedAttributes(self): - if isinstance(self.__item, int): - self.__fetchItemInfo() - return self.__itemModifiedAttributes def clear(self): @@ -67,25 +56,16 @@ class Ship(ItemAttrShortcut, HandledItem): if effect.runTime == runTime and effect.isType("passive"): effect.handler(fit, self, ("ship",)) - def checkModeItem(self, item): - """ - Checks if provided item is a valid mode. - - If ship has modes, and current item is not valid, return forced mode - else if mode is valid, return Mode - else if ship does not have modes, return None - - @todo: rename this - """ + def validateModeItem(self, item): + """ Checks if provided item is a valid mode """ items = self.__modeItems - if items != None: - if item == None or item not in items: - # We have a tact dessy, but mode is None or not valid. Force new mode + if items is not None: + # if we have items, then we are in a tactical destroyer and must have a mode + if item is None or item not in items: + # If provided item is invalid mode, force new one return Mode(items[0]) - elif item in items: - # We have a valid mode - return Mode(item) + return Mode(item) return None @property @@ -96,21 +76,17 @@ class Ship(ItemAttrShortcut, HandledItem): def modes(self): return [Mode(item) for item in self.__modeItems] if self.__modeItems else None - def _getModeItems(self): + def __getModeItems(self): """ Returns a list of valid mode items for ship. Note that this returns the valid Item objects, not the Mode objects. Returns None if not a t3 dessy """ - # @todo: is there a better way to determine this that isn't hardcoded groupIDs? - if self.item.groupID != 1305: + if self.item.group.name != "Tactical Destroyer": return None - modeGroupID = 1306 - import eos.db - items = [] - g = eos.db.getGroup(modeGroupID, eager=("items.icon", "items.attributes")) + g = eos.db.getGroup("Ship Modifiers", eager=("items.icon", "items.attributes")) for item in g.items: # Rely on name detection because race is not reliable if item.name.lower().startswith(self.item.name.lower()): diff --git a/eos/slotFill.py b/eos/slotFill.py deleted file mode 100644 index ce00a07ae..000000000 --- a/eos/slotFill.py +++ /dev/null @@ -1,223 +0,0 @@ -#=============================================================================== -# Copyright (C) 2010 Diego Duclos -# -# This file is part of eos. -# -# eos is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# eos is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with eos. If not, see . -#=============================================================================== - -from eos.types import Slot, Fit, Module, State -import random -import copy -import math -import bisect -import itertools -import time - -class SlotFill(object): - def __init__(self, original, modules, attributeWeights=None, propertyWeights=None, specificWeights=None, defaultState = State.ACTIVE): - self.original = original - self.attributeWeights = attributeWeights or {} - self.propertyWeights = propertyWeights or {} - self.specificWeights = specificWeights or [] - self.state = State.ACTIVE - self.modules = map(self.__newModule, modules) - - def __newModule(self, item): - m = Module(item) - m.state = self.state - return m - - def __getMetaParent(self, item): - metaGroup = item.metaGroup - return item if metaGroup is None else metaGroup.parent - - def fitness(self, fit, chromosome): - modList = fit.modules - modAttr = fit.ship.getModifiedItemAttr - - modList.extend(chromosome) - fit.clear() - fit.calculateModifiedAttributes() - - if not fit.fits: - del modList[-len(chromosome):] - return 0 - - weight = 0 - for attr, value in self.attributeWeights.iteritems(): - weight += modAttr(attr) * (value if value >= 0 else 1.0 / -value) - - for prop, value in self.propertyWeights.iteritems(): - weight += getattr(fit, prop) * (value if value >= 0 else 1.0 / -value) - - for specific in self.specificWeights: - weight += specific(fit) - - totalVars = (fit.ship.getModifiedItemAttr("powerOutput"), - fit.ship.getModifiedItemAttr("cpuOutput"), - fit.ship.getModifiedItemAttr('upgradeCapacity')) - - usedVars = (fit.pgUsed, fit.cpuUsed, fit.calibrationUsed) - - total = 0 - used = 0 - for tv, uv in zip(totalVars, usedVars): - if uv > tv: - del modList[-len(chromosome):] - return 0 - - del modList[-len(chromosome):] - - - return weight - - - def run(self, elite = 0.05, crossoverChance = 0.8, slotMutationChance = 0.5, typeMutationChance = 0.5): - #Use a copy of the original for all our calcs. We don't want to damage it - fit = copy.deepcopy(self.original) - fit.unfill() - - #First of all, lets check the number of slots we got to play with - chromLength = -1 - slotAmounts = {} - for type in Slot.getTypes(): - slot = Slot.getValue(type) - amount = fit.getSlotsFree(slot) - if amount > 0: - slotAmounts[slot] = amount - - chromLength += amount - - if not slotAmounts: - #Nothing to do, joy - return - - slotModules = {} - metaModules = {} - - for slotType in slotAmounts: - slotModules[slotType] = modules = [] - - for module in self.modules: - #Store the variations of each base for ease and speed - metaParent = self.__getMetaParent(module.item) - metaList = metaModules.get(metaParent) - if metaList is None: - metaList = metaModules[metaParent] = [] - metaList.append(module) - - #Sort stuff by slotType for ease and speed - slot = module.slot - if slot in slotModules: - slotModules[slot].append(module) - - for slotType, modules in slotModules.iteritems(): - if len(modules) == 0: - chromLength -= slotAmounts[slotType] - del slotAmounts[slotType] - - #Now, we need an initial set, first thing to do is decide how big that set will be - setSize = 10 - - #Grab some variables locally for performance improvements - rchoice = random.choice - rrandom = random.random - rrandint = random.randint - bbisect = bisect.bisect - ccopy = copy.copy - - #Get our list for storage of our chromosomes - chromosomes = [] - - # Helpers - weigher = lambda chromosome: (self.fitness(fit, chromosome), chromosome) - keyer = lambda info: info[0] - - eliteCutout = int(math.floor(setSize * (1 - elite))) - lastEl = setSize - 1 - - #Generate our initial set entirely randomly - #Subtelies to take in mind: - # * modules of the same slotType are kept together for easy cross-overing - state = self.state - for _ in xrange(setSize): - chrom = [] - for type, amount in slotAmounts.iteritems(): - for _ in xrange(amount): - chrom.append(rchoice(slotModules[type])) - - chromosomes.append(weigher(chrom)) - - #Sort our initial set - chromosomes.sort(key=keyer) - currentGeneration = chromosomes - - #Yield the best result from our initial set, this is gonna be pretty bad - yield currentGeneration[lastEl] - - #Setup's done, now we can actualy apply our genetic algorithm to optimize all this - while True: - moo = time.time() - #First thing we do, we're gonna be elitair - #Grab the top x%, we'll put em in the next generation - nextGeneration = [] - for i in xrange(lastEl, eliteCutout - 1, -1): - nextGeneration.append(currentGeneration[i]) - - #Figure out our ratios to do our roulette wheel - fitnessList = map(keyer, currentGeneration) - totalFitness = float(sum(fitnessList)) - - curr = 0 - ratios = [] - for fitness in fitnessList: - curr += fitness - ratios.append(curr / (totalFitness or 1)) - - t = 0 - #Do our pairing - for _ in xrange(0, eliteCutout): - # Crossover chance - mother = currentGeneration[bbisect(ratios, rrandom())][1] - father = currentGeneration[bbisect(ratios, rrandom())][1] - if rrandom() <= crossoverChance: - crosspoint = rrandint(0, chromLength) - luke = mother[:crosspoint] + father[crosspoint:] - else: - luke = father - - #Chance for slot mutation - if rrandom() <= slotMutationChance: - target = rrandint(0, chromLength) - mod = luke[target] - luke[target] = rchoice(slotModules[mod.slot]) - - if rrandom() <= typeMutationChance: - #Mutation of an item to another one of the same type - target = rrandint(0, chromLength) - mod = luke[target] - vars = metaModules[self.__getMetaParent(mod.item)] - luke[target] = rchoice(vars) - - tt = time.time() - nextGeneration.append(weigher(luke)) - t += time.time() - tt - - print "time spent weighing: ", t - - nextGeneration.sort(key=keyer) - currentGeneration = nextGeneration - print "total time spent this iteration:", time.time() - moo - yield currentGeneration[lastEl] diff --git a/gui/bitmapLoader.py b/gui/bitmapLoader.py index e0a773b98..50feb8777 100644 --- a/gui/bitmapLoader.py +++ b/gui/bitmapLoader.py @@ -71,10 +71,23 @@ def getBitmap(name,location): # print "#BMPs:%d - Current took: %.8f" % (cachedBitmapsCount,time.clock() - start) return bmp +def stripPath(fname): + """ + Here we extract 'core' of icon name. Path and + extension are sometimes specified in database + but we don't need them. + """ + # Path before the icon file name + fname = fname.split('/')[-1] + # Extension + fname = fname.rsplit('.', 1)[0] + return fname + def getImage(name, location): if location in locationMap: if location == "pack": location = locationMap[location] + name = stripPath(name) filename = "icon{0}.png".format(name) path = os.path.join(location, filename) else: diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index 60468ec9a..87f506ba6 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -56,6 +56,9 @@ class PFGeneralPref ( PreferenceView): self.cbShowTooltip = wx.CheckBox( panel, wx.ID_ANY, u"Show tab tooltips", wx.DefaultPosition, wx.DefaultSize, 0 ) mainSizer.Add( self.cbShowTooltip, 0, wx.ALL|wx.EXPAND, 5 ) + self.cbMarketShortcuts = wx.CheckBox( panel, wx.ID_ANY, u"Show market shortcuts", wx.DefaultPosition, wx.DefaultSize, 0 ) + mainSizer.Add( self.cbMarketShortcuts, 0, wx.ALL|wx.EXPAND, 5 ) + defCharSizer = wx.BoxSizer( wx.HORIZONTAL ) self.sFit = service.Fit.getInstance() @@ -69,6 +72,7 @@ class PFGeneralPref ( PreferenceView): self.cbCompactSkills.SetValue(self.sFit.serviceFittingOptions["compactSkills"] or False) self.cbReopenFits.SetValue(self.openFitsSettings["enabled"]) self.cbShowTooltip.SetValue(self.sFit.serviceFittingOptions["showTooltip"] or False) + self.cbMarketShortcuts.SetValue(self.sFit.serviceFittingOptions["showMarketShortcuts"] or False) self.cbGlobalChar.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalCharStateChange) self.cbGlobalDmgPattern.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalDmgPatternStateChange) @@ -79,6 +83,7 @@ class PFGeneralPref ( PreferenceView): self.cbCompactSkills.Bind(wx.EVT_CHECKBOX, self.onCBCompactSkills) self.cbReopenFits.Bind(wx.EVT_CHECKBOX, self.onCBReopenFits) self.cbShowTooltip.Bind(wx.EVT_CHECKBOX, self.onCBShowTooltip) + self.cbMarketShortcuts.Bind(wx.EVT_CHECKBOX, self.onCBShowShortcuts) self.cbRackLabels.Enable(self.sFit.serviceFittingOptions["rackSlots"] or False) @@ -135,6 +140,9 @@ class PFGeneralPref ( PreferenceView): def onCBShowTooltip(self, event): self.sFit.serviceFittingOptions["showTooltip"] = self.cbShowTooltip.GetValue() + def onCBShowShortcuts(self, event): + self.sFit.serviceFittingOptions["showMarketShortcuts"] = self.cbMarketShortcuts.GetValue() + def getImage(self): return bitmapLoader.getBitmap("prefs_settings", "icons") diff --git a/gui/builtinStatsViews/priceViewFull.py b/gui/builtinStatsViews/priceViewFull.py index 2aef192c8..f1d3fbd1d 100644 --- a/gui/builtinStatsViews/priceViewFull.py +++ b/gui/builtinStatsViews/priceViewFull.py @@ -30,37 +30,13 @@ class PriceViewFull(StatsView): def __init__(self, parent): StatsView.__init__(self) self.parent = parent - self._timerId = wx.NewId() - self._timer = None - self.parent.Bind(wx.EVT_TIMER, self.OnTimer) - self._timerRunsBeforeUpdate = 60 - self._timerRuns = 0 - self._timerIdUpdate = wx.NewId() - self._timerUpdate = None self._cachedShip = 0 self._cachedFittings = 0 self._cachedTotal = 0 - def OnTimer(self, event): - if self._timerId == event.GetId(): - if self._timerRuns >= self._timerRunsBeforeUpdate: - self._timerRuns = 0 - self._timer.Stop() - self.refreshPanel(self.fit) - else: - self.labelEMStatus.SetLabel("Prices update retry in: %d seconds" %(self._timerRunsBeforeUpdate - self._timerRuns)) - self._timerRuns += 1 - if self._timerIdUpdate == event.GetId(): - self._timerUpdate.Stop() - self.labelEMStatus.SetLabel("") - def getHeaderText(self, fit): return "Price" - def getTextExtentW(self, text): - width, height = self.parent.GetTextExtent(text) - return width - def populatePanel(self, contentPanel, headerPanel): contentSizer = contentPanel.GetSizer() self.panel = contentPanel @@ -111,22 +87,11 @@ class PriceViewFull(StatsView): for cargo in fit.cargo: for _ in xrange(cargo.amount): typeIDs.append(cargo.itemID) - if self._timer: - if self._timer.IsRunning(): - self._timer.Stop() + sMkt = service.Market.getInstance() sMkt.getPrices(typeIDs, self.processPrices) self.labelEMStatus.SetLabel("Updating prices...") - if not self._timerUpdate: - self._timerUpdate = wx.Timer(self.parent, self._timerIdUpdate) - if self._timerUpdate: - if not self._timerUpdate.IsRunning(): - self._timerUpdate.Start(1000) - else: - if self._timer: - if self._timer.IsRunning(): - self._timer.Stop() self.labelEMStatus.SetLabel("") self.labelPriceShip.SetLabel("0.0 ISK") self.labelPriceFittings.SetLabel("0.0 ISK") @@ -136,20 +101,10 @@ class PriceViewFull(StatsView): def processPrices(self, prices): shipPrice = prices[0].price - if shipPrice == None: - if not self._timer: - self._timer = wx.Timer(self.parent, self._timerId) - self._timer.Start(1000) - self._timerRuns = 0 - else: - if self._timer: - self._timer.Stop() - - self.labelEMStatus.SetLabel("") - - if shipPrice == None: - shipPrice = 0 modPrice = sum(map(lambda p: p.price or 0, prices[1:])) + + self.labelEMStatus.SetLabel("") + if self._cachedShip != shipPrice: self.labelPriceShip.SetLabel("%s ISK" % formatAmount(shipPrice, 3, 3, 9, currency=True)) self.labelPriceShip.SetToolTip(wx.ToolTip(locale.format('%.2f', shipPrice, 1))) diff --git a/gui/builtinStatsViews/resistancesViewFull.py b/gui/builtinStatsViews/resistancesViewFull.py index e98696fb6..2b0d94b5f 100644 --- a/gui/builtinStatsViews/resistancesViewFull.py +++ b/gui/builtinStatsViews/resistancesViewFull.py @@ -196,8 +196,9 @@ class ResistancesViewFull(StatsView): lbl = getattr(self, "labelResistance%sEhp" % tankType.capitalize()) if ehp is not None: total += ehp[tankType] + rrFactor = fit.ehp[tankType] / fit.hp[tankType] lbl.SetLabel(formatAmount(ehp[tankType], 3, 0, 9)) - lbl.SetToolTip(wx.ToolTip("%s: %d" % (tankType.capitalize(), ehp[tankType]))) + lbl.SetToolTip(wx.ToolTip("%s: %d\nResist Multiplier: x%.2f" % (tankType.capitalize(), ehp[tankType], rrFactor))) else: lbl.SetLabel("0") diff --git a/gui/builtinViewColumns/baseName.py b/gui/builtinViewColumns/baseName.py index 0966c9c7d..a6b3fe49f 100644 --- a/gui/builtinViewColumns/baseName.py +++ b/gui/builtinViewColumns/baseName.py @@ -55,6 +55,15 @@ class BaseName(ViewColumn): return stuff.item.name else: item = getattr(stuff, "item", stuff) + + if service.Fit.getInstance().serviceFittingOptions["showMarketShortcuts"]: + marketShortcut = getattr(item, "marketShortcut", None) + + if marketShortcut: + # use unicode subscript to display shortcut value + shortcut = unichr(marketShortcut+8320)+u" " + return shortcut+item.name + return item.name BaseName.register() diff --git a/gui/builtinViewColumns/price.py b/gui/builtinViewColumns/price.py index 5fe6ef954..c046a4b55 100644 --- a/gui/builtinViewColumns/price.py +++ b/gui/builtinViewColumns/price.py @@ -33,13 +33,13 @@ class Price(ViewColumn): self.imageId = fittingView.imageList.GetImageIndex("totalPrice_small", "icons") def getText(self, stuff): - if stuff.item is None: + if stuff.item is None or stuff.item.group.name == "Ship Modifiers": return "" sMkt = service.Market.getInstance() price = sMkt.getPriceNow(stuff.item.ID) - if not price or not price.price: + if not price or not price.price or not price.isValid: return False price = price.price # Set new price variable with what we need @@ -50,12 +50,17 @@ class Price(ViewColumn): return formatAmount(price, 3, 3, 9, currency=True) def delayedText(self, mod, display, colItem): - def callback(requests): - price = requests[0].price - colItem.SetText(formatAmount(price, 3, 3, 9, currency=True) if price else "") + sMkt = service.Market.getInstance() + def callback(item): + price = sMkt.getPriceNow(item.ID) + text = formatAmount(price.price, 3, 3, 9, currency=True) if price.price else "" + if price.failed: text += " (!)" + colItem.SetText(text) + display.SetItem(colItem) - service.Market.getInstance().getPrices([mod.item.ID], callback) + + sMkt.waitForPrice(mod.item, callback) def getImageId(self, mod): return -1 diff --git a/gui/display.py b/gui/display.py index 0cfc28854..707623581 100644 --- a/gui/display.py +++ b/gui/display.py @@ -250,7 +250,7 @@ class Display(wx.ListCtrl): newText = col.getText(st) if newText is False: col.delayedText(st, self, colItem) - newText = "" + newText = u"\u21bb" newImageId = col.getImageId(st) diff --git a/gui/droneView.py b/gui/droneView.py index ddf16bf72..f71c7cb25 100644 --- a/gui/droneView.py +++ b/gui/droneView.py @@ -36,7 +36,8 @@ class DroneViewDrop(wx.PyDropTarget): def OnData(self, x, y, t): if self.GetData(): - self.dropFn(x, y, int(self.dropData.GetText())) + data = self.dropData.GetText().split(':') + self.dropFn(x, y, data) return t class DroneView(d.Display): @@ -72,7 +73,7 @@ class DroneView(d.Display): self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag) - self.SetDropTarget(DroneViewDrop(self.mergeDrones)) + self.SetDropTarget(DroneViewDrop(self.handleDragDrop)) def OnLeaveWindow(self, event): self.SetToolTip(None) @@ -117,17 +118,27 @@ class DroneView(d.Display): row = event.GetIndex() if row != -1: data = wx.PyTextDataObject() - data.SetText(str(self.GetItemData(row))) + data.SetText("drone:"+str(row)) dropSource = wx.DropSource(self) dropSource.SetData(data) res = dropSource.DoDragDrop() - def mergeDrones(self, x, y, itemID): - srcRow = self.FindItemData(-1,itemID) - dstRow, _ = self.HitTest((x, y)) - if srcRow != -1 and dstRow != -1: - self._merge(srcRow, dstRow) + def handleDragDrop(self, x, y, data): + ''' + Handles dragging of items from various pyfa displays which support it + + data is list with two indices: + data[0] is hard-coded str of originating source + data[1] is typeID or index of data we want to manipulate + ''' + if data[0] == "drone": # we want to merge drones + srcRow = int(data[1]) + dstRow, _ = self.HitTest((x, y)) + if srcRow != -1 and dstRow != -1: + self._merge(srcRow, dstRow) + elif data[0] == "market": + wx.PostEvent(self.mainFrame, mb.ItemSelected(itemID=int(data[1]))) def _merge(self, src, dst): sFit = service.Fit.getInstance() diff --git a/gui/mainFrame.py b/gui/mainFrame.py index d563b8c20..6f89df091 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -39,7 +39,7 @@ import gui.globalEvents as GE from gui import bitmapLoader from gui.mainMenuBar import MainMenuBar from gui.additionsPane import AdditionsPane -from gui.marketBrowser import MarketBrowser +from gui.marketBrowser import MarketBrowser, ItemSelected from gui.multiSwitch import MultiSwitch from gui.statsPane import StatsPane from gui.shipBrowser import ShipBrowser, FitSelected, ImportSelected, Stage3Selected @@ -144,6 +144,7 @@ class MainFrame(wx.Frame): self.marketBrowser = MarketBrowser(self.notebookBrowsers) self.notebookBrowsers.AddPage(self.marketBrowser, "Market", tabImage = marketImg, showClose = False) + self.marketBrowser.splitter.SetSashPosition(self.marketHeight) self.shipBrowser = ShipBrowser(self.notebookBrowsers) self.notebookBrowsers.AddPage(self.shipBrowser, "Ships", tabImage = shipBrowserImg, showClose = False) @@ -158,7 +159,7 @@ class MainFrame(wx.Frame): self.splitter.SplitVertically(self.notebookBrowsers, self.FitviewAdditionsPanel) self.splitter.SetMinimumPaneSize(204) - self.splitter.SetSashPosition(300) + self.splitter.SetSashPosition(self.browserWidth) cstatsSizer = wx.BoxSizer(wx.VERTICAL) @@ -225,7 +226,7 @@ class MainFrame(wx.Frame): def LoadMainFrameAttribs(self): - mainFrameDefaultAttribs = {"wnd_width": 1000, "wnd_height": 680, "wnd_maximized": False} + mainFrameDefaultAttribs = {"wnd_width": 1000, "wnd_height": 680, "wnd_maximized": False, "browser_width": 300, "market_height": 0} self.mainFrameAttribs = service.SettingsProvider.getInstance().getSettings("pyfaMainWindowAttribs", mainFrameDefaultAttribs) if self.mainFrameAttribs["wnd_maximized"]: @@ -239,6 +240,9 @@ class MainFrame(wx.Frame): self.SetSize((width, height)) self.SetMinSize((mainFrameDefaultAttribs["wnd_width"], mainFrameDefaultAttribs["wnd_height"])) + self.browserWidth = self.mainFrameAttribs["browser_width"] + self.marketHeight = self.mainFrameAttribs["market_height"] + def UpdateMainFrameAttribs(self): if self.IsIconized(): return @@ -248,6 +252,9 @@ class MainFrame(wx.Frame): self.mainFrameAttribs["wnd_height"] = height self.mainFrameAttribs["wnd_maximized"] = self.IsMaximized() + self.mainFrameAttribs["browser_width"] = self.notebookBrowsers.GetSize()[0] + self.mainFrameAttribs["market_height"] = self.marketBrowser.marketView.GetSize()[1] + def SetActiveStatsWindow(self, wnd): self.activeStatsWnd = wnd @@ -421,12 +428,6 @@ class MainFrame(wx.Frame): ctabnext = wx.NewId() ctabprev = wx.NewId() - self.additionstab1 = wx.NewId() - self.additionstab2 = wx.NewId() - self.additionstab3 = wx.NewId() - self.additionstab4 = wx.NewId() - self.additionstab5 = wx.NewId() - # Close Page self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId) self.Bind(wx.EVT_MENU, self.HAddPage, id = self.addPageId) @@ -435,12 +436,6 @@ class MainFrame(wx.Frame): self.Bind(wx.EVT_MENU, self.CTabNext, id = ctabnext) self.Bind(wx.EVT_MENU, self.CTabPrev, id = ctabprev) - self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab1) - self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab2) - self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab3) - self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab4) - self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab5) - actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId), (wx.ACCEL_CMD, ord('T'), self.addPageId), @@ -460,41 +455,43 @@ class MainFrame(wx.Frame): (wx.ACCEL_CMD, wx.WXK_TAB, ctabnext), (wx.ACCEL_CMD | wx.ACCEL_SHIFT, wx.WXK_TAB, ctabprev), - # Ctrl+age(Up/Down) + # Ctrl+Page(Up/Down) (wx.ACCEL_CTRL, wx.WXK_PAGEDOWN, ctabnext), (wx.ACCEL_CTRL, wx.WXK_PAGEUP, ctabprev), (wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext), - (wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev), - - (wx.ACCEL_CTRL, ord('1'), self.additionstab1), - (wx.ACCEL_CTRL, ord('2'), self.additionstab2), - (wx.ACCEL_CTRL, ord('3'), self.additionstab3), - (wx.ACCEL_CTRL, ord('4'), self.additionstab4), - (wx.ACCEL_CTRL, ord('5'), self.additionstab5), - (wx.ACCEL_CMD, ord('1'), self.additionstab1), - (wx.ACCEL_CMD, ord('2'), self.additionstab2), - (wx.ACCEL_CMD, ord('3'), self.additionstab3), - (wx.ACCEL_CMD, ord('4'), self.additionstab4), - (wx.ACCEL_CMD, ord('5'), self.additionstab5) + (wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev) ] + + # Ctrl/Cmd+# for addition pane selection + self.additionsSelect = [] + for i in range(0, self.additionsPane.notebook.GetPageCount()): + self.additionsSelect.append(wx.NewId()) + self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id=self.additionsSelect[i]) + actb.append((wx.ACCEL_CMD, i+49, self.additionsSelect[i])) + actb.append((wx.ACCEL_CTRL, i+49, self.additionsSelect[i])) + + # Alt+1-9 for market item selection + self.itemSelect = [] + for i in range(0, 9): + self.itemSelect.append(wx.NewId()) + self.Bind(wx.EVT_MENU, self.ItemSelect, id = self.itemSelect[i]) + actb.append((wx.ACCEL_ALT, i + 49, self.itemSelect[i])) + atable = wx.AcceleratorTable(actb) self.SetAcceleratorTable(atable) def AdditionsTabSelect(self, event): - selTab = None - if event.GetId() == self.additionstab1: - selTab = 0 - if event.GetId() == self.additionstab2: - selTab = 1 - if event.GetId() == self.additionstab3: - selTab = 2 - if event.GetId() == self.additionstab4: - selTab = 3 - if event.GetId() == self.additionstab5: - selTab = 4 - if selTab is not None: + selTab = self.additionsSelect.index(event.GetId()) + + if selTab <= self.additionsPane.notebook.GetPageCount(): self.additionsPane.notebook.SetSelection(selTab) + def ItemSelect(self, event): + selItem = self.itemSelect.index(event.GetId()) + + if selItem < len(self.marketBrowser.itemView.active): + wx.PostEvent(self, ItemSelected(itemID=self.marketBrowser.itemView.active[selItem].ID)) + def CTabNext(self, event): self.fitMultiSwitch.NextPage() diff --git a/gui/marketBrowser.py b/gui/marketBrowser.py index 771c7ab8a..1c8329a1b 100644 --- a/gui/marketBrowser.py +++ b/gui/marketBrowser.py @@ -438,6 +438,11 @@ class ItemView(d.Display): self.metalvls = sMkt.directAttrRequest(items, attrs) # Re-sort stuff items.sort(key=self.itemSort) + + for i, item in enumerate(items[:9]): + # set shortcut info for first 9 modules + item.marketShortcut = i+1 + d.Display.refresh(self, items) def makeReverseMetaMap(self): diff --git a/icons/refresh_small.png b/icons/refresh_small.png new file mode 100644 index 000000000..d3087dfc9 Binary files /dev/null and b/icons/refresh_small.png differ diff --git a/scripts/icons_update.py b/scripts/icons_update.py new file mode 100644 index 000000000..c152f55be --- /dev/null +++ b/scripts/icons_update.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 + +""" +This script updates only market/item icons. +""" + + +import argparse +import os +import re +import sqlite3 + +from PIL import Image + + +parser = argparse.ArgumentParser(description='This script updates module icons for pyfa') +parser.add_argument('-i', '--icons', required=True, type=str, help='path to unpacked Icons folder from CCP\'s image export') +args = parser.parse_args() + + +script_dir = os.path.dirname(os.path.abspath(__file__)) +db_path = os.path.abspath(os.path.join(script_dir, '..', 'staticdata', 'eve.db')) +icons_dir = os.path.abspath(os.path.join(script_dir, '..', 'staticdata', 'icons')) +export_dir = os.path.abspath(os.path.expanduser(os.path.join(args.icons, 'items'))) + + +db = sqlite3.connect(db_path) +cursor = db.cursor() + +ICON_SIZE = (16, 16) + +ITEM_CATEGORIES = ( + 6, # Ship + 7, # Module + 8, # Charge + 16, # Skill + 18, # Drone + 20, # Implant + 32 # Subsystem +) +MARKET_ROOTS = { + 9, # Modules + 1111, # Rigs + 157, # Drones + 11, # Ammo + 1112, # Subsystems + 24, # Implants & Boosters + 404 # Deployables +} + +# Add children to market group list +# {parent: {children}} +mkt_tree = {} +for row in cursor.execute('select marketGroupID, parentGroupID from invmarketgroups'): + parent = row[1] + # We have all the root groups in the set we need anyway + if not parent: + continue + child = row[0] + children = mkt_tree.setdefault(parent, set()) + children.add(child) + +# Traverse the tree we just composed to add all children for all needed roots +def get_children(parent): + children = set() + for child in mkt_tree.get(parent, ()): + children.add(child) + children.update(get_children(child)) + return children + + +market_groups = set() +for root in MARKET_ROOTS: + market_groups.add(root) + market_groups.update(get_children(root)) + + +query_items = 'select distinct i.iconFile from icons as i inner join invtypes as it on it.iconID = i.iconID inner join invgroups as ig on it.groupID = ig.groupID where ig.categoryID in ({})'.format(', '.join(str(i) for i in ITEM_CATEGORIES)) +query_groups = 'select distinct i.iconFile from icons as i inner join invgroups as ig on ig.iconID = i.iconID where ig.categoryID in ({})'.format(', '.join(str(i) for i in ITEM_CATEGORIES)) +query_cats = 'select distinct i.iconFile from icons as i inner join invcategories as ic on ic.iconID = i.iconID where ic.categoryID in ({})'.format(', '.join(str(i) for i in ITEM_CATEGORIES)) +query_market = 'select distinct i.iconFile from icons as i inner join invmarketgroups as img on img.iconID = i.iconID where img.marketGroupID in ({})'.format(', '.join(str(i) for i in market_groups)) +query_attrib = 'select distinct i.iconFile from icons as i inner join dgmattribs as da on da.iconID = i.iconID' + + +needed = set() +existing = set() +export = {} + + +def strip_path(fname): + """ + Here we extract 'core' of icon name. Path and + extension are sometimes specified in database + but we don't need them. + """ + # Path before the icon file name + fname = fname.split('/')[-1] + # Extension + fname = fname.rsplit('.', 1)[0] + return fname + + +def unzero(fname): + """ + Get rid of leading zeros in triplet. They are often specified in DB + but almost never in actual files. + """ + m = re.match(r'^(?P[^_\.]+)_((?P\d+)_)?(?P[^_\.]+)(?P\..*)?$', fname) + if m: + prefix = m.group('prefix') + size = m.group('size') + suffix = m.group('suffix') + tail = m.group('tail') + try: + prefix = int(prefix) + except (TypeError, ValueError): + pass + try: + size = int(size) + except (TypeError, ValueError): + pass + try: + suffix = int(suffix) + except (TypeError, ValueError): + pass + if size is None: + fname = '{}_{}{}'.format(prefix, suffix, tail) + else: + fname = '{}_{}_{}{}'.format(prefix, size, suffix, tail) + return fname + else: + return fname + +for query in (query_items, query_groups, query_cats, query_market, query_attrib): + for row in cursor.execute(query): + fname = row[0] + if not fname: + continue + fname = strip_path(fname) + needed.add(fname) + +for fname in os.listdir(icons_dir): + if not os.path.isfile(os.path.join(icons_dir, fname)): + continue + if not fname.startswith('icon') or not fname.endswith('.png'): + continue + fname = strip_path(fname) + # Get rid of "icon" prefix as well + fname = re.sub('^icon', '', fname) + existing.add(fname) + + +for fname in os.listdir(export_dir): + if not os.path.isfile(os.path.join(export_dir, fname)): + continue + stripped = strip_path(fname) + stripped = unzero(stripped) + # Icons in export often specify size in their name, but references often use + # convention without size specification + sizeless = re.sub('^(?P[^_]+)_(?P\d+)_(?P[^_]+)$', r'\1_\3', stripped) + # Often items referred to with 01_01 format, + fnames = export.setdefault(stripped, set()) + fnames.add(fname) + fnames = export.setdefault(sizeless, set()) + fnames.add(fname) + + +def crop_image(img): + w, h = img.size + if h == w: + return img + normal = min(h, w) + diff_w = w - normal + diff_h = h - normal + crop_top = diff_h // 2 + crop_bot = diff_h // 2 + diff_h % 2 + crop_left = diff_w // 2 + crop_right = diff_w // 2 + diff_w % 2 + box = (crop_left, crop_top, w - crop_right, h - crop_bot) + return img.crop(box) + + +def get_icon_file(request): + """ + Get the iconFile field value and find proper + icon for it. Return as PIL image object down- + scaled for use in pyfa. + """ + rq = strip_path(request) + rq = unzero(rq) + try: + fnames = export[rq] + except KeyError: + return None + # {(h, w): source full path} + sizes = {} + for fname in fnames: + fullpath = os.path.join(export_dir, fname) + img = Image.open(fullpath) + sizes[img.size] = fullpath + # Try to return image which is already in necessary format + try: + fullpath = sizes[ICON_SIZE] + # Otherwise, convert biggest image + except KeyError: + fullpath = sizes[max(sizes)] + img = Image.open(fullpath) + img = crop_image(img) + img.thumbnail(ICON_SIZE, Image.ANTIALIAS) + else: + img = Image.open(fullpath) + return img + + +toremove = existing.difference(needed) +toupdate = existing.intersection(needed) +toadd = needed.difference(existing) + + +if toremove: + print('Some icons are not used and will be removed:') + for fname in sorted(toremove): + fullname = 'icon{}.png'.format(fname) + print(' {}'.format(fullname)) + fullpath = os.path.join(icons_dir, fullname) + os.remove(fullpath) + +if toupdate: + print('Updating {} icons...'.format(len(toupdate))) + missing = set() + for fname in sorted(toupdate): + icon = get_icon_file(fname) + if icon is None: + missing.add(fname) + continue + fullname = 'icon{}.png'.format(fname) + fullpath = os.path.join(icons_dir, fullname) + icon.save(fullpath, 'PNG') + if missing: + print(' {} icons are missing in export:'.format(len(missing))) + for fname in sorted(missing): + print(' {}'.format(fname)) + +if toadd: + print('Adding {} icons...'.format(len(toadd))) + missing = set() + for fname in sorted(toadd): + icon = get_icon_file(fname) + if icon is None: + missing.add(fname) + continue + fullname = 'icon{}.png'.format(fname) + fullpath = os.path.join(icons_dir, fullname) + icon.save(fullpath, 'PNG') + if missing: + print(' {} icons are missing in export:'.format(len(missing))) + for fname in sorted(missing): + print(' {}'.format(fname)) diff --git a/scripts/jsonToSql.py b/scripts/jsonToSql.py index 07b6cbc94..d4de50755 100755 --- a/scripts/jsonToSql.py +++ b/scripts/jsonToSql.py @@ -52,11 +52,11 @@ def main(db, json_path): "dgmtypeeffects": eos.gamedata.Effect, "dgmunits": eos.gamedata.Unit, "icons": eos.gamedata.Icon, - "invcategories": eos.gamedata.Category, - "invgroups": eos.gamedata.Group, + "evecategories": eos.gamedata.Category, + "evegroups": eos.gamedata.Group, "invmetagroups": eos.gamedata.MetaGroup, "invmetatypes": eos.gamedata.MetaType, - "invtypes": eos.gamedata.Item, + "evetypes": eos.gamedata.Item, "phbtraits": eos.gamedata.Traits, "phbmetadata": eos.gamedata.MetaData, "mapbulk_marketGroups": eos.gamedata.MarketGroup @@ -74,16 +74,16 @@ def main(db, json_path): "displayName_en-us": "displayName" }, #icons??? - "invcategories": { + "evecategories": { "categoryName_en-us": "categoryName" }, - "invgroups": { + "evegroups": { "groupName_en-us": "groupName" }, "invmetagroups": { "metaGroupName_en-us": "metaGroupName" }, - "invtypes": { + "evetypes": { "typeName_en-us": "typeName", "description_en-us": "description" }, @@ -95,6 +95,12 @@ def main(db, json_path): } + rowsInValues = ( + "evetypes", + "evegroups", + "evecategories" + ) + def convertIcons(data): new = [] for k, v in data.items(): @@ -133,7 +139,7 @@ def main(db, json_path): def convertTypes(typesData): """ - Add factionID column to invtypes table. + Add factionID column to evetypes table. """ factionMap = {} with open(os.path.join(jsonPath, "fsdTypeOverrides.json")) as f: @@ -152,30 +158,28 @@ def main(db, json_path): for jsonName, cls in tables.iteritems(): with open(os.path.join(jsonPath, "{}.json".format(jsonName))) as f: tableData = json.load(f) + if jsonName in rowsInValues: + tableData = list(tableData.values()) if jsonName == "icons": tableData = convertIcons(tableData) if jsonName == "phbtraits": tableData = convertTraits(tableData) - if jsonName == "invtypes": + if jsonName == "evetypes": tableData = convertTypes(tableData) data[jsonName] = tableData # Set with typeIDs which we will have in our database - invTypes = { # Sometimes CCP unpublishes some items we want to have published, we - # can do it here - 31906, # Federation Navy 200mm Steel Plates - 31904, # Imperial Navy 200mm Steel Plates - 28782, # Syndicate 200mm Steel Plates - } - for row in data["invtypes"]: + # can do it here - just add them to initial set + eveTypes = set() + for row in data["evetypes"]: # 1306 - group Ship Modifiers, for items like tactical t3 ship modes if (row["published"] or row['groupID'] == 1306): - invTypes.add(row["typeID"]) + eveTypes.add(row["typeID"]) # ignore checker def isIgnored(file, row): - if file in ("invtypes", "dgmtypeeffects", "dgmtypeattribs", "invmetatypes") and row['typeID'] not in invTypes: + if file in ("evetypes", "dgmtypeeffects", "dgmtypeattribs", "invmetatypes") and row['typeID'] not in eveTypes: return True return False diff --git a/scripts/renders_update.py b/scripts/renders_update.py new file mode 100644 index 000000000..dfe88e448 --- /dev/null +++ b/scripts/renders_update.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 + +""" +This script updates ship renders and removes unused ones. +""" + + +import argparse +import os +import re +import sqlite3 + +from PIL import Image + + +parser = argparse.ArgumentParser(description='This script updates ship renders for pyfa') +parser.add_argument('-r', '--renders', required=True, type=str, help='path to unpacked Renders folder from CCP\'s image export') +args = parser.parse_args() + + +script_dir = os.path.dirname(os.path.abspath(__file__)) +db_path = os.path.abspath(os.path.join(script_dir, '..', 'staticdata', 'eve.db')) +icons_dir = os.path.abspath(os.path.join(script_dir, '..', 'staticdata', 'icons', 'ships')) +export_dir = os.path.abspath(os.path.expanduser(args.renders)) + + +db = sqlite3.connect(db_path) +cursor = db.cursor() + +RENDER_SIZE = (32, 32) + + +query_ships = 'select it.typeID from invtypes as it inner join invgroups as ig on it.groupID = ig.groupID where ig.categoryID = 6' + + +needed = set() +existing = set() +export = set() + + +for row in cursor.execute(query_ships): + needed.add(row[0]) + +for container, filedir in ( + (existing, icons_dir), + (export, export_dir) +): + for fname in os.listdir(filedir): + if not os.path.isfile(os.path.join(filedir, fname)): + continue + m = re.match(r'^(?P\d+)\.png', fname) + if not m: + continue + container.add(int(m.group('typeid'))) + +toremove = existing.difference(needed) +toupdate = existing.intersection(needed) +toadd = needed.difference(existing) + + +def crop_image(img): + w, h = img.size + if h == w: + return img + normal = min(h, w) + diff_w = w - normal + diff_h = h - normal + crop_top = diff_h // 2 + crop_bot = diff_h // 2 + diff_h % 2 + crop_left = diff_w // 2 + crop_right = diff_w // 2 + diff_w % 2 + box = (crop_left, crop_top, w - crop_right, h - crop_bot) + return img.crop(box) + + +def get_render(type_id): + fname = '{}.png'.format(type_id) + fullpath = os.path.join(export_dir, fname) + img = Image.open(fullpath) + if img.size != RENDER_SIZE: + img = crop_image(img) + img.thumbnail(RENDER_SIZE, Image.ANTIALIAS) + return img + + +if toremove: + print('Some renders are not used and will be removed:') + for type_id in sorted(toremove): + fullname = '{}.png'.format(type_id) + print(' {}'.format(fullname)) + fullpath = os.path.join(icons_dir, fullname) + os.remove(fullpath) + +if toupdate: + print('Updating {} renders...'.format(len(toupdate))) + missing = toupdate.difference(export) + toupdate.intersection_update(export) + for type_id in sorted(toupdate): + render = get_render(type_id) + fname = '{}.png'.format(type_id) + fullpath = os.path.join(icons_dir, fname) + render.save(fullpath, 'PNG') + if missing: + print(' {} renders are missing in export:'.format(len(missing))) + for type_id in sorted(missing): + print(' {}.png'.format(type_id)) + +if toadd: + print('Adding {} renders...'.format(len(toadd))) + missing = toadd.difference(export) + toadd.intersection_update(export) + for type_id in sorted(toadd): + render = get_render(type_id) + fname = '{}.png'.format(type_id) + fullpath = os.path.join(icons_dir, fname) + render.save(fullpath, 'PNG') + if missing: + print(' {} renders are missing in export:'.format(len(missing))) + for type_id in sorted(missing): + print(' {}.png'.format(type_id)) diff --git a/service/fit.py b/service/fit.py index 5360a376a..f20d9183a 100644 --- a/service/fit.py +++ b/service/fit.py @@ -97,7 +97,8 @@ class Fit(object): "rackSlots": True, "rackLabels": True, "compactSkills": True, - "showTooltip": True} + "showTooltip": True, + "showMarketShortcuts": False} self.serviceFittingOptions = SettingsProvider.getInstance().getSettings( "pyfaServiceFittingOptions", serviceFittingDefaultOptions) @@ -149,8 +150,8 @@ class Fit(object): return fit.modules[pos] def newFit(self, shipID, name=None): - fit = eos.types.Fit() - fit.ship = eos.types.Ship(eos.db.getItem(shipID)) + ship = eos.types.Ship(eos.db.getItem(shipID)) + fit = eos.types.Fit(ship) fit.name = name if name is not None else "New %s" % fit.ship.item.name fit.damagePattern = self.pattern fit.targetResists = self.targetResists @@ -274,7 +275,6 @@ class Fit(object): except ValueError: return False - fit.implants.freeSlot(implant) fit.implants.append(implant) self.recalc(fit) return True @@ -300,7 +300,6 @@ class Fit(object): except ValueError: return False - fit.boosters.freeSlot(booster) fit.boosters.append(booster) self.recalc(fit) return True @@ -893,7 +892,7 @@ class Fit(object): State.ONLINE: State.ACTIVE} # Just in case def __getProposedState(self, mod, click, proposedState=None): - if mod.slot in (Slot.RIG, Slot.SUBSYSTEM) or mod.isEmpty: + if mod.slot is Slot.SUBSYSTEM or mod.isEmpty: return State.ONLINE currState = mod.state diff --git a/service/market.py b/service/market.py index 0ea234aaf..e2d103874 100644 --- a/service/market.py +++ b/service/market.py @@ -71,6 +71,7 @@ class ShipBrowserWorkerThread(threading.Thread): class PriceWorkerThread(threading.Thread): def run(self): self.queue = Queue.Queue() + self.wait = {} self.processUpdates() def processUpdates(self): @@ -86,9 +87,21 @@ class PriceWorkerThread(threading.Thread): wx.CallAfter(callback) queue.task_done() + # After we fetch prices, go through the list of waiting items and call their callbacks + for price in requests: + callbacks = self.wait.pop(price.typeID, None) + if callbacks: + for callback in callbacks: + wx.CallAfter(callback) + def trigger(self, prices, callbacks): self.queue.put((callbacks, prices)) + def setToWait(self, itemID, callback): + if itemID not in self.wait: + self.wait[itemID] = [] + self.wait[itemID].append(callback) + class SearchWorkerThread(threading.Thread): def run(self): self.cv = threading.Condition() @@ -218,9 +231,8 @@ class Market(): "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 - "Federation Navy 200mm Steel Plates": True, # Accidentally unpublished by CCP - "Imperial Navy 200mm Steel Plates": True, # Accidentally unpublished by CCP - "Syndicate 200mm Steel Plates": True, # Accidentally unpublished by CCP + "Imp": False, # AT13 prize, not a real ship yet + "Fiend": False, # AT13 prize, not a real ship yet } # do not publish ships that we convert @@ -693,10 +705,6 @@ class Market(): self.priceCache[typeID] = price - if not price.isValid: - # if the price has expired - price.price = None - return price def getPricesNow(self, typeIDs): @@ -719,6 +727,21 @@ class Market(): self.priceWorkerThread.trigger(requests, cb) + def waitForPrice(self, item, callback): + """ + Wait for prices to be fetched and callback when finished. This is used with the column prices for modules. + Instead of calling them individually, we set them to wait until the entire fit price is called and calculated + (see GH #290) + """ + + def cb(): + try: + callback(item) + except: + pass + + self.priceWorkerThread.setToWait(item.ID, cb) + def clearPriceCache(self): self.priceCache.clear() deleted_rows = eos.db.clearPrices() diff --git a/service/prefetch.py b/service/prefetch.py index 56c8b5684..664e5168c 100644 --- a/service/prefetch.py +++ b/service/prefetch.py @@ -55,4 +55,4 @@ else: # If database does not exist, do not worry about migration. Simply # create and set version eos.db.saveddata_meta.create_all() - eos.db.saveddata_engine.execute('PRAGMA user_version = %d'%config.dbversion) + eos.db.saveddata_engine.execute('PRAGMA user_version = {}'.format(migration.getAppVersion())) diff --git a/service/price.py b/service/price.py index aefd1b14f..71858cf7c 100644 --- a/service/price.py +++ b/service/price.py @@ -52,7 +52,7 @@ class Price(): item = eos.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.marketGroupID: + if item is not None and item.marketGroupID: toRequest.add(typeID) # Do not waste our time if all items are not on the market @@ -71,7 +71,6 @@ class Price(): # Attempt to send request and process it try: - len(priceMap) network = service.Network.getInstance() data = network.request(baseurl, network.PRICES, data) xml = minidom.parse(data) @@ -102,7 +101,7 @@ class Price(): for typeID in priceMap.keys(): priceobj = priceMap[typeID] priceobj.time = time.time() + TIMEOUT - priceobj.failed = None + priceobj.failed = True del priceMap[typeID] except: # all other errors will pass and continue onward to the REREQUEST delay @@ -111,6 +110,5 @@ class Price(): # if we get to this point, then we've got an error. Set to REREQUEST delay for typeID in priceMap.keys(): priceobj = priceMap[typeID] - priceobj.price = 0 priceobj.time = time.time() + REREQUEST - priceobj.failed = None + priceobj.failed = True diff --git a/staticdata/eve.db b/staticdata/eve.db index 032a2a351..cca9a894f 100644 Binary files a/staticdata/eve.db and b/staticdata/eve.db differ diff --git a/staticdata/icons/icon07_12.png b/staticdata/icons/icon07_12.png new file mode 100644 index 000000000..8a158dcf4 Binary files /dev/null and b/staticdata/icons/icon07_12.png differ diff --git a/staticdata/icons/icon107_1.png b/staticdata/icons/icon107_1.png new file mode 100644 index 000000000..5bdcbda55 Binary files /dev/null and b/staticdata/icons/icon107_1.png differ diff --git a/staticdata/icons/icon107_10.png b/staticdata/icons/icon107_10.png new file mode 100644 index 000000000..872e70d68 Binary files /dev/null and b/staticdata/icons/icon107_10.png differ diff --git a/staticdata/icons/icon107_11.png b/staticdata/icons/icon107_11.png new file mode 100644 index 000000000..17eba237c Binary files /dev/null and b/staticdata/icons/icon107_11.png differ diff --git a/staticdata/icons/icon107_12.png b/staticdata/icons/icon107_12.png new file mode 100644 index 000000000..1bf43c7e2 Binary files /dev/null and b/staticdata/icons/icon107_12.png differ diff --git a/staticdata/icons/icon107_2.png b/staticdata/icons/icon107_2.png new file mode 100644 index 000000000..90ce2cd0a Binary files /dev/null and b/staticdata/icons/icon107_2.png differ diff --git a/staticdata/icons/icon107_3.png b/staticdata/icons/icon107_3.png new file mode 100644 index 000000000..d70c59fb8 Binary files /dev/null and b/staticdata/icons/icon107_3.png differ diff --git a/staticdata/icons/icon107_64_4.png b/staticdata/icons/icon107_64_4.png new file mode 100644 index 000000000..e37ca2e9f Binary files /dev/null and b/staticdata/icons/icon107_64_4.png differ diff --git a/staticdata/icons/icon108_1.png b/staticdata/icons/icon108_1.png new file mode 100644 index 000000000..956260537 Binary files /dev/null and b/staticdata/icons/icon108_1.png differ diff --git a/staticdata/icons/icon108_10.png b/staticdata/icons/icon108_10.png new file mode 100644 index 000000000..c703e4d99 Binary files /dev/null and b/staticdata/icons/icon108_10.png differ diff --git a/staticdata/icons/icon108_11.png b/staticdata/icons/icon108_11.png new file mode 100644 index 000000000..ae5978b60 Binary files /dev/null and b/staticdata/icons/icon108_11.png differ diff --git a/staticdata/icons/icon108_12.png b/staticdata/icons/icon108_12.png new file mode 100644 index 000000000..951655dc8 Binary files /dev/null and b/staticdata/icons/icon108_12.png differ diff --git a/staticdata/icons/icon108_13.png b/staticdata/icons/icon108_13.png new file mode 100644 index 000000000..8b11794f5 Binary files /dev/null and b/staticdata/icons/icon108_13.png differ diff --git a/staticdata/icons/icon108_14.png b/staticdata/icons/icon108_14.png new file mode 100644 index 000000000..51a91a0c3 Binary files /dev/null and b/staticdata/icons/icon108_14.png differ diff --git a/staticdata/icons/icon108_15.png b/staticdata/icons/icon108_15.png new file mode 100644 index 000000000..cef54cad9 Binary files /dev/null and b/staticdata/icons/icon108_15.png differ diff --git a/staticdata/icons/icon108_16.png b/staticdata/icons/icon108_16.png new file mode 100644 index 000000000..4f347cd98 Binary files /dev/null and b/staticdata/icons/icon108_16.png differ diff --git a/staticdata/icons/icon108_17.png b/staticdata/icons/icon108_17.png new file mode 100644 index 000000000..3207e1d58 Binary files /dev/null and b/staticdata/icons/icon108_17.png differ diff --git a/staticdata/icons/icon108_18.png b/staticdata/icons/icon108_18.png new file mode 100644 index 000000000..e5e54a340 Binary files /dev/null and b/staticdata/icons/icon108_18.png differ diff --git a/staticdata/icons/icon108_19.png b/staticdata/icons/icon108_19.png new file mode 100644 index 000000000..00e17b419 Binary files /dev/null and b/staticdata/icons/icon108_19.png differ diff --git a/staticdata/icons/icon108_2.png b/staticdata/icons/icon108_2.png new file mode 100644 index 000000000..32f0cea32 Binary files /dev/null and b/staticdata/icons/icon108_2.png differ diff --git a/staticdata/icons/icon108_20.png b/staticdata/icons/icon108_20.png new file mode 100644 index 000000000..e83f0782d Binary files /dev/null and b/staticdata/icons/icon108_20.png differ diff --git a/staticdata/icons/icon108_21.png b/staticdata/icons/icon108_21.png new file mode 100644 index 000000000..083848e56 Binary files /dev/null and b/staticdata/icons/icon108_21.png differ diff --git a/staticdata/icons/icon108_3.png b/staticdata/icons/icon108_3.png new file mode 100644 index 000000000..f303c38fd Binary files /dev/null and b/staticdata/icons/icon108_3.png differ diff --git a/staticdata/icons/icon108_4.png b/staticdata/icons/icon108_4.png new file mode 100644 index 000000000..e559fe0f6 Binary files /dev/null and b/staticdata/icons/icon108_4.png differ diff --git a/staticdata/icons/icon108_6.png b/staticdata/icons/icon108_6.png new file mode 100644 index 000000000..f33e4b32b Binary files /dev/null and b/staticdata/icons/icon108_6.png differ diff --git a/staticdata/icons/icon108_64_22.png b/staticdata/icons/icon108_64_22.png new file mode 100644 index 000000000..c67d4e601 Binary files /dev/null and b/staticdata/icons/icon108_64_22.png differ diff --git a/staticdata/icons/icon108_7.png b/staticdata/icons/icon108_7.png new file mode 100644 index 000000000..8e746ff5c Binary files /dev/null and b/staticdata/icons/icon108_7.png differ diff --git a/staticdata/icons/icon108_8.png b/staticdata/icons/icon108_8.png new file mode 100644 index 000000000..32efa4c3c Binary files /dev/null and b/staticdata/icons/icon108_8.png differ diff --git a/staticdata/icons/icon109_64_1.png b/staticdata/icons/icon109_64_1.png new file mode 100644 index 000000000..7563b2257 Binary files /dev/null and b/staticdata/icons/icon109_64_1.png differ diff --git a/staticdata/icons/icon109_64_2.png b/staticdata/icons/icon109_64_2.png new file mode 100644 index 000000000..57d3548da Binary files /dev/null and b/staticdata/icons/icon109_64_2.png differ diff --git a/staticdata/icons/icon118_64_7.png b/staticdata/icons/icon118_64_7.png new file mode 100644 index 000000000..4c1185f39 Binary files /dev/null and b/staticdata/icons/icon118_64_7.png differ diff --git a/staticdata/icons/icon119_1.png b/staticdata/icons/icon119_1.png new file mode 100644 index 000000000..f1a7fb1e7 Binary files /dev/null and b/staticdata/icons/icon119_1.png differ diff --git a/staticdata/icons/icon1337_21.png b/staticdata/icons/icon1337_21.png new file mode 100644 index 000000000..c9133cbb8 Binary files /dev/null and b/staticdata/icons/icon1337_21.png differ diff --git a/staticdata/icons/icon1337_22.png b/staticdata/icons/icon1337_22.png new file mode 100644 index 000000000..84ce0dcf8 Binary files /dev/null and b/staticdata/icons/icon1337_22.png differ diff --git a/staticdata/icons/icon5_64_17.png b/staticdata/icons/icon5_64_17.png new file mode 100644 index 000000000..60182777f Binary files /dev/null and b/staticdata/icons/icon5_64_17.png differ diff --git a/staticdata/icons/icon5_64_18.png b/staticdata/icons/icon5_64_18.png new file mode 100644 index 000000000..b9e9d3b5a Binary files /dev/null and b/staticdata/icons/icon5_64_18.png differ diff --git a/staticdata/icons/icon5_64_19.png b/staticdata/icons/icon5_64_19.png new file mode 100644 index 000000000..e47daa18b Binary files /dev/null and b/staticdata/icons/icon5_64_19.png differ diff --git a/staticdata/icons/icon5_64_20.png b/staticdata/icons/icon5_64_20.png new file mode 100644 index 000000000..1f3fd652f Binary files /dev/null and b/staticdata/icons/icon5_64_20.png differ diff --git a/staticdata/icons/icon5_64_21.png b/staticdata/icons/icon5_64_21.png new file mode 100644 index 000000000..926361acd Binary files /dev/null and b/staticdata/icons/icon5_64_21.png differ diff --git a/staticdata/icons/iconIcon_64px_Fireworks.png b/staticdata/icons/iconIcon_64px_Fireworks.png new file mode 100644 index 000000000..7bd17da63 Binary files /dev/null and b/staticdata/icons/iconIcon_64px_Fireworks.png differ diff --git a/staticdata/icons/iconremote_armor_repair.png b/staticdata/icons/iconremote_armor_repair.png new file mode 100644 index 000000000..16d4337ee Binary files /dev/null and b/staticdata/icons/iconremote_armor_repair.png differ diff --git a/staticdata/icons/iconremote_hull_repairer.png b/staticdata/icons/iconremote_hull_repairer.png new file mode 100644 index 000000000..2360bfb28 Binary files /dev/null and b/staticdata/icons/iconremote_hull_repairer.png differ diff --git a/staticdata/icons/ships/32811.png b/staticdata/icons/ships/32811.png new file mode 100644 index 000000000..308088b91 Binary files /dev/null and b/staticdata/icons/ships/32811.png differ diff --git a/staticdata/icons/ships/34151.png b/staticdata/icons/ships/34151.png new file mode 100644 index 000000000..bf8a8d147 Binary files /dev/null and b/staticdata/icons/ships/34151.png differ diff --git a/staticdata/icons/ships/34496.png b/staticdata/icons/ships/34496.png new file mode 100644 index 000000000..cd6d661af Binary files /dev/null and b/staticdata/icons/ships/34496.png differ diff --git a/staticdata/icons/ships/34828.png b/staticdata/icons/ships/34828.png index cfd751e1d..fcc20574e 100644 Binary files a/staticdata/icons/ships/34828.png and b/staticdata/icons/ships/34828.png differ diff --git a/staticdata/icons/ships/35683.png b/staticdata/icons/ships/35683.png new file mode 100644 index 000000000..bc0dd4276 Binary files /dev/null and b/staticdata/icons/ships/35683.png differ diff --git a/staticdata/icons/ships/35779.png b/staticdata/icons/ships/35779.png new file mode 100644 index 000000000..c0fde6d67 Binary files /dev/null and b/staticdata/icons/ships/35779.png differ diff --git a/staticdata/icons/ships/35781.png b/staticdata/icons/ships/35781.png new file mode 100644 index 000000000..79d5e6670 Binary files /dev/null and b/staticdata/icons/ships/35781.png differ