diff --git a/config.py b/config.py index 6f1047302..382ee6e1b 100644 --- a/config.py +++ b/config.py @@ -17,7 +17,7 @@ debug = False # Defines if our saveddata will be in pyfa root or not saveInRoot = False -logLevel = logging.WARN +logLevel = logging.DEBUG # Version data version = "1.13.3" @@ -32,6 +32,21 @@ staticPath = None saveDB = None gameDB = None + +class StreamToLogger(object): + """ + Fake file-like stream object that redirects writes to a logger instance. + From: http://www.electricmonk.nl/log/2011/08/14/redirect-stdout-and-stderr-to-a-logger-in-python/ + """ + def __init__(self, logger, log_level=logging.INFO): + self.logger = logger + self.log_level = log_level + self.linebuf = '' + + def write(self, buf): + for line in buf.rstrip().splitlines(): + self.logger.log(self.log_level, line.rstrip()) + def __createDirs(path): if not os.path.exists(path): os.makedirs(path) @@ -72,15 +87,14 @@ def defPaths(): logging.info("Starting pyfa") - # Redirect stderr to file if we're requested to do so - stderrToFile = getattr(configforced, "stderrToFile", None) - if stderrToFile is True: - sys.stderr = open(os.path.join(savePath, "error_log.txt"), "w") + if hasattr(sys, 'frozen'): + stdout_logger = logging.getLogger('STDOUT') + sl = StreamToLogger(stdout_logger, logging.INFO) + sys.stdout = sl - # Same for stdout - stdoutToFile = getattr(configforced, "stdoutToFile", None) - if stdoutToFile is True: - sys.stdout = open(os.path.join(savePath, "output_log.txt"), "w") + stderr_logger = logging.getLogger('STDERR') + sl = StreamToLogger(stderr_logger, logging.ERROR) + sys.stderr = sl # Static EVE Data from the staticdata repository, should be in the staticdata # directory in our pyfa directory diff --git a/eos/db/migrations/upgrade10.py b/eos/db/migrations/upgrade10.py new file mode 100644 index 000000000..0bfb0f0ee --- /dev/null +++ b/eos/db/migrations/upgrade10.py @@ -0,0 +1,16 @@ +""" +Migration 10 + +- Adds active attribute to projected fits +""" + +import sqlalchemy + +def upgrade(saveddata_engine): + # Update projectedFits schema to include active attribute + try: + saveddata_engine.execute("SELECT active FROM projectedFits LIMIT 1") + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE projectedFits ADD COLUMN active BOOLEAN") + saveddata_engine.execute("UPDATE projectedFits SET active = 1") + saveddata_engine.execute("UPDATE projectedFits SET amount = 1") diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 0c5edaee9..a40d8c7fb 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -17,9 +17,11 @@ # along with eos. If not, see . #=============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey, String, Boolean -from sqlalchemy.orm import relation, mapper +from sqlalchemy import * +from sqlalchemy.orm import * from sqlalchemy.sql import and_ +from sqlalchemy.ext.associationproxy import association_proxy +from sqlalchemy.orm.collections import attribute_mapped_collection from eos.db import saveddata_meta from eos.db.saveddata.module import modules_table @@ -45,33 +47,119 @@ fits_table = Table("fits", saveddata_meta, projectedFits_table = Table("projectedFits", saveddata_meta, Column("sourceID", ForeignKey("fits.ID"), primary_key = True), Column("victimID", ForeignKey("fits.ID"), primary_key = True), - Column("amount", Integer)) + Column("amount", Integer, nullable = False, default = 1), + Column("active", Boolean, nullable = False, default = 1), +) + +class ProjectedFit(object): + def __init__(self, sourceID, source_fit, amount=1, active=True): + self.sourceID = sourceID + self.source_fit = source_fit + self.active = active + self.__amount = amount + + @reconstructor + def init(self): + if self.source_fit.isInvalid: + # Very rare for this to happen, but be prepared for it + eos.db.saveddata_session.delete(self.source_fit) + eos.db.saveddata_session.flush() + eos.db.saveddata_session.refresh(self.victim_fit) + + # We have a series of setters and getters here just in case someone + # downgrades and screws up the table with NULL values + @property + def amount(self): + return self.__amount or 1 + + @amount.setter + def amount(self, amount): + self.__amount = amount + + def __repr__(self): + return "ProjectedFit(sourceID={}, victimID={}, amount={}, active={}) at {}".format( + self.sourceID, self.victimID, self.amount, self.active, hex(id(self)) + ) + +Fit._Fit__projectedFits = association_proxy( + "victimOf", # look at the victimOf association... + "source_fit", # .. and return the source fits + creator=lambda sourceID, source_fit: ProjectedFit(sourceID, source_fit) +) + mapper(Fit, fits_table, - properties = {"_Fit__modules" : relation(Module, collection_class = HandledModuleList, - primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False), - order_by = modules_table.c.position, cascade='all, delete, delete-orphan'), - "_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 = 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 = 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', backref='fit', single_parent=True, - primaryjoin = fitImplants_table.c.fitID == fits_table.c.ID, - secondaryjoin = fitImplants_table.c.implantID == Implant.ID, - secondary = fitImplants_table), - "_Fit__character" : relation(Character, backref = "fits"), - "_Fit__damagePattern" : relation(DamagePattern), - "_Fit__targetResists" : relation(TargetResists), - "_Fit__projectedFits" : relation(Fit, - primaryjoin = projectedFits_table.c.victimID == fits_table.c.ID, - secondaryjoin = fits_table.c.ID == projectedFits_table.c.sourceID, - secondary = projectedFits_table, - collection_class = HandledProjectedFitList) - }) + properties = { + "_Fit__modules": relation( + Module, + collection_class=HandledModuleList, + primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False), + order_by=modules_table.c.position, + cascade='all, delete, delete-orphan'), + "_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=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=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', + backref='fit', + single_parent=True, + primaryjoin=fitImplants_table.c.fitID == fits_table.c.ID, + secondaryjoin=fitImplants_table.c.implantID == Implant.ID, + secondary=fitImplants_table), + "_Fit__character": relation( + Character, + backref="fits"), + "_Fit__damagePattern": relation(DamagePattern), + "_Fit__targetResists": relation(TargetResists), + "projectedOnto": relationship( + ProjectedFit, + primaryjoin=projectedFits_table.c.sourceID == fits_table.c.ID, + backref='source_fit', + collection_class=attribute_mapped_collection('victimID'), + cascade='all, delete, delete-orphan'), + "victimOf": relationship( + ProjectedFit, + primaryjoin=fits_table.c.ID == projectedFits_table.c.victimID, + backref='victim_fit', + collection_class=attribute_mapped_collection('sourceID'), + cascade='all, delete, delete-orphan'), + } +) + +mapper(ProjectedFit, projectedFits_table, + properties = { + "_ProjectedFit__amount": projectedFits_table.c.amount, + } +) diff --git a/eos/effectHandlerHelpers.py b/eos/effectHandlerHelpers.py index 5e1997ce7..3bf576a4c 100644 --- a/eos/effectHandlerHelpers.py +++ b/eos/effectHandlerHelpers.py @@ -232,11 +232,6 @@ class HandledProjectedDroneList(HandledDroneCargoList): if proj.isInvalid or not proj.item.isType("projected"): self.remove(proj) -class HandledProjectedFitList(HandledList): - def append(self, proj): - proj.projected = True - list.append(self, proj) - class HandledItem(object): def preAssignItemAttr(self, *args, **kwargs): self.itemModifiedAttributes.preAssign(*args, **kwargs) diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py index 79d871ce5..09497be2a 100644 --- a/eos/modifiedAttributeDict.py +++ b/eos/modifiedAttributeDict.py @@ -217,13 +217,16 @@ class ModifiedAttributeDict(collections.MutableMapping): if attributeName not in self.__affectedBy: self.__affectedBy[attributeName] = {} affs = self.__affectedBy[attributeName] + origin = self.fit.getOrigin() + fit = origin if origin and origin != self.fit else self.fit # If there's no set for current fit in dictionary, create it - if self.fit not in affs: - affs[self.fit] = [] + if fit not in affs: + affs[fit] = [] # Reassign alias to list - affs = affs[self.fit] + affs = affs[fit] # Get modifier which helps to compose 'Affected by' map modifier = self.fit.getModifier() + # Add current affliction to list affs.append((modifier, operation, bonus, used)) diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 3efb5b75e..2fc2cf824 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -286,5 +286,10 @@ class Skill(HandledItem): copy = Skill(self.item, self.level, self.__ro) return copy + def __repr__(self): + return "Skill(ID={}, name={}) at {}".format( + self.item.ID, self.item.name, hex(id(self)) + ) + class ReadOnlyException(Exception): pass diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index 35c332b36..9d689a743 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -29,6 +29,9 @@ from eos.saveddata.module import State from eos.saveddata.mode import Mode import eos.db import time +import copy +from utils.timer import Timer + import logging logger = logging.getLogger(__name__) @@ -40,14 +43,6 @@ except ImportError: class Fit(object): """Represents a fitting, with modules, ship, implants, etc.""" - EXTRA_ATTRIBUTES = {"armorRepair": 0, - "hullRepair": 0, - "shieldRepair": 0, - "maxActiveDrones": 0, - "maxTargetsLockedFromSkills": 2, - "droneControlRange": 20000, - "cloaked": False, - "siege": False} PEAK_RECHARGE = 0.25 @@ -61,7 +56,7 @@ class Fit(object): self.__cargo = HandledDroneCargoList() self.__implants = HandledImplantBoosterList() self.__boosters = HandledImplantBoosterList() - self.__projectedFits = HandledProjectedFitList() + #self.__projectedFits = {} self.__projectedModules = HandledProjectedModList() self.__projectedDrones = HandledProjectedDroneList() self.__character = None @@ -88,6 +83,10 @@ class Fit(object): try: self.__ship = Ship(item) + # @todo extra attributes is now useless, however it set to be + # the same as ship attributes for ease (so we don't have to + # change all instances in source). Remove this at some point + self.extraAttributes = self.__ship.itemModifiedAttributes except ValueError: logger.error("Item (id: %d) is not a Ship", self.shipID) return @@ -124,8 +123,6 @@ class Fit(object): self.boostsFits = set() self.gangBoosts = None self.ecmProjectedStr = 1 - self.extraAttributes = ModifiedAttributeDict(self) - self.extraAttributes.original = self.EXTRA_ATTRIBUTES @property def targetResists(self): @@ -178,8 +175,11 @@ class Fit(object): def ship(self, ship): self.__ship = ship self.shipID = ship.item.ID if ship is not None else None - # set mode of new ship - self.mode = self.ship.validateModeItem(None) if ship is not None else None + if ship is not None: + # set mode of new ship + self.mode = self.ship.validateModeItem(None) if ship is not None else None + # set fit attributes the same as ship + self.extraAttributes = self.ship.itemModifiedAttributes @property def drones(self): @@ -207,7 +207,12 @@ class Fit(object): @property def projectedFits(self): - return self.__projectedFits + # only in extreme edge cases will the fit be invalid, but to be sure do + # not return them. + return [fit for fit in self.__projectedFits.values() if not fit.isInvalid] + + def getProjectionInfo(self, fitID): + return self.projectedOnto.get(fitID, None) @property def projectedDrones(self): @@ -326,7 +331,7 @@ class Fit(object): if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key) else: return val - def clear(self): + def clear(self, projected=False): self.__effectiveTank = None self.__weaponDPS = None self.__minerYield = None @@ -346,15 +351,36 @@ class Fit(object): del self.__calculatedTargets[:] del self.__extraDrains[:] - if self.ship is not None: self.ship.clear() - c = chain(self.modules, self.drones, self.boosters, self.implants, self.projectedDrones, self.projectedModules, self.projectedFits, (self.character, self.extraAttributes)) + if self.ship: + self.ship.clear() + + c = chain( + self.modules, + self.drones, + self.boosters, + self.implants, + self.projectedDrones, + self.projectedModules, + (self.character, self.extraAttributes), + ) + for stuff in c: - if stuff is not None and stuff != self: stuff.clear() + if stuff is not None and stuff != self: + stuff.clear() + + # If this is the active fit that we are clearing, not a projected fit, + # then this will run and clear the projected ships and flag the next + # iteration to skip this part to prevent recursion. + if not projected: + for stuff in self.projectedFits: + if stuff is not None and stuff != self: + stuff.clear(projected=True) #Methods to register and get the thing currently affecting the fit, #so we can correctly map "Affected By" - def register(self, currModifier): + def register(self, currModifier, origin=None): self.__modifier = currModifier + self.__origin = origin if hasattr(currModifier, "itemModifiedAttributes"): currModifier.itemModifiedAttributes.fit = self if hasattr(currModifier, "chargeModifiedAttributes"): @@ -363,98 +389,129 @@ class Fit(object): def getModifier(self): return self.__modifier + def getOrigin(self): + return self.__origin + + def __calculateGangBoosts(self, runTime): + logger.debug("Applying gang boosts in `%s` runtime for %s", runTime, repr(self)) + for name, info in self.gangBoosts.iteritems(): + # Unpack all data required to run effect properly + effect, thing = info[1] + if effect.runTime == runTime: + context = ("gang", thing.__class__.__name__.lower()) + if isinstance(thing, Module): + if effect.isType("offline") or (effect.isType("passive") and thing.state >= State.ONLINE) or \ + (effect.isType("active") and thing.state >= State.ACTIVE): + # Run effect, and get proper bonuses applied + try: + self.register(thing) + effect.handler(self, thing, context) + except: + pass + else: + # Run effect, and get proper bonuses applied + try: + self.register(thing) + effect.handler(self, thing, context) + except: + pass + def calculateModifiedAttributes(self, targetFit=None, withBoosters=False, dirtyStorage=None): - refreshBoosts = False - if withBoosters is True: - refreshBoosts = True - if dirtyStorage is not None and self.ID in dirtyStorage: - refreshBoosts = True - if dirtyStorage is not None: - dirtyStorage.update(self.boostsFits) - if self.fleet is not None and refreshBoosts is True: - self.gangBoosts = self.fleet.recalculateLinear(withBoosters=withBoosters, dirtyStorage=dirtyStorage) + timer = Timer('Fit: {}, {}'.format(self.ID, self.name), logger) + logger.debug("Starting fit calculation on: %s, withBoosters: %s", repr(self), withBoosters) + + shadow = False + if targetFit: + logger.debug("Applying projections to target: %s", repr(targetFit)) + projectionInfo = self.getProjectionInfo(targetFit.ID) + logger.debug("ProjectionInfo: %s", projectionInfo) + if self == targetFit: + copied = self # original fit + shadow = True + self = copy.deepcopy(self) + self.fleet = copied.fleet + logger.debug("Handling self projection - making shadow copy of fit. %s => %s", repr(copied), repr(self)) + # we delete the fit because when we copy a fit, flush() is + # called to properly handle projection updates. However, we do + # not want to save this fit to the database, so simply remove it + eos.db.saveddata_session.delete(self) + + if self.fleet is not None and withBoosters is True: + logger.debug("Fleet is set, gathering gang boosts") + self.gangBoosts = self.fleet.recalculateLinear(withBoosters=withBoosters) + timer.checkpoint("Done calculating gang boosts for %s"%repr(self)) elif self.fleet is None: self.gangBoosts = None - if dirtyStorage is not None: - try: - dirtyStorage.remove(self.ID) - except KeyError: - pass + # If we're not explicitly asked to project fit onto something, # set self as target fit if targetFit is None: targetFit = self - forceProjected = False - # Else, we're checking all target projectee fits - elif targetFit not in self.__calculatedTargets: - self.__calculatedTargets.append(targetFit) - targetFit.calculateModifiedAttributes(dirtyStorage=dirtyStorage) - forceProjected = True - # Or do nothing if target fit is calculated + projected = False else: - return + projected = True # If fit is calculated and we have nothing to do here, get out - if self.__calculated == True and forceProjected == False: + + # A note on why projected fits don't get to return here. If we return + # here, the projection afflictions will not be run as they are + # intertwined into the regular fit calculations. So, even if the fit has + # been calculated, we need to recalculate it again just to apply the + # projections. This is in contract to gang boosts, which are only + # calculated once, and their items are then looped and accessed with + # self.gangBoosts.iteritems() + # We might be able to exit early in the fit calculations if we separate + # projections from the normal fit calculations. But we must ensure that + # projection have modifying stuff applied, such as gang boosts and other + # local modules that may help + if self.__calculated and not projected: + logger.debug("Fit has already been calculated and is not projected, returning: %s", repr(self)) return # Mark fit as calculated self.__calculated = True - # There's a few things to keep in mind here - # 1: Early effects first, then regular ones, then late ones, regardless of anything else - # 2: Some effects aren't implemented - # 3: Some effects are implemented poorly and will just explode on us - # 4: Errors should be handled gracefully and preferably without crashing unless serious for runTime in ("early", "normal", "late"): - # Build a little chain of stuff - # Avoid adding projected drones and modules when fit is projected onto self - # TODO: remove this workaround when proper self-projection using virtual duplicate fits is implemented - if forceProjected is True: - # if fit is being projected onto another fit - c = chain((self.character, self.ship), self.drones, self.boosters, self.appliedImplants, self.modules) - else: - c = chain((self.character, self.ship, self.mode), self.drones, self.boosters, self.appliedImplants, self.modules, - self.projectedDrones, self.projectedModules) + c = chain( + (self.character, self.ship), + self.drones, + self.boosters, + self.appliedImplants, + self.modules + ) + if not projected: + # if not a projected fit, add a couple of more things + c = chain(c, (self.mode,), self.projectedDrones, self.projectedModules) + + # We calculate gang bonuses first so that projected fits get them if self.gangBoosts is not None: - contextMap = {Skill: "skill", - Ship: "ship", - Module: "module", - Implant: "implant"} - for name, info in self.gangBoosts.iteritems(): - # Unpack all data required to run effect properly - effect, thing = info[1] - if effect.runTime == runTime: - context = ("gang", contextMap[type(thing)]) - if isinstance(thing, Module): - if effect.isType("offline") or (effect.isType("passive") and thing.state >= State.ONLINE) or \ - (effect.isType("active") and thing.state >= State.ACTIVE): - # Run effect, and get proper bonuses applied - try: - self.register(thing) - effect.handler(self, thing, context) - except: - pass - else: - # Run effect, and get proper bonuses applied - try: - self.register(thing) - effect.handler(self, thing, context) - except: - pass + self.__calculateGangBoosts(runTime) for item in c: - # Registering the item about to affect the fit allows us to track "Affected By" relations correctly + # Registering the item about to affect the fit allows us to + # track "Affected By" relations correctly if item is not None: self.register(item) item.calculateModifiedAttributes(self, runTime, False) - if forceProjected is True: - targetFit.register(item) - item.calculateModifiedAttributes(targetFit, runTime, True) + if projected is True: + for _ in xrange(projectionInfo.amount): + targetFit.register(item, origin=self) + item.calculateModifiedAttributes(targetFit, runTime, True) - for fit in self.projectedFits: - fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage) + timer.checkpoint('Done with runtime: %s'%runTime) + + # Only apply projected fits if fit it not projected itself. + if not projected: + for fit in self.projectedFits: + if fit.getProjectionInfo(self.ID).active: + fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage) + + timer.checkpoint('Done with fit calculation') + + if shadow: + logger.debug("Delete shadow fit object") + del self def fill(self): """ @@ -927,6 +984,19 @@ class Fit(object): c.append(deepcopy(i, memo)) for fit in self.projectedFits: - copy.projectedFits.append(fit) + copy.__projectedFits[fit.ID] = fit + # this bit is required -- see GH issue # 83 + eos.db.saveddata_session.flush() + eos.db.saveddata_session.refresh(fit) return copy + + def __repr__(self): + return "Fit(ID={}, ship={}, name={}) at {}".format( + self.ID, self.ship.item.name, self.name, hex(id(self)) + ) + + def __str__(self): + return "{} ({})".format( + self.name, self.ship.item.name + ) diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index a4fdbec07..32a28c28e 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -639,6 +639,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): copy.state = self.state return copy + def __repr__(self): + if self.item: + return "Module(ID={}, name={}) at {}".format( + self.item.ID, self.item.name, hex(id(self)) + ) + else: + return "EmptyModule() at {}".format(hex(id(self))) + class Rack(Module): ''' This is simply the Module class named something else to differentiate diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index 86123bfc1..901aa2607 100644 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -26,6 +26,17 @@ import logging logger = logging.getLogger(__name__) class Ship(ItemAttrShortcut, HandledItem): + EXTRA_ATTRIBUTES = { + "armorRepair": 0, + "hullRepair": 0, + "shieldRepair": 0, + "maxActiveDrones": 0, + "maxTargetsLockedFromSkills": 2, + "droneControlRange": 20000, + "cloaked": False, + "siege": False + } + def __init__(self, item): if item.category.name != "Ship": @@ -34,7 +45,8 @@ class Ship(ItemAttrShortcut, HandledItem): self.__item = item self.__modeItems = self.__getModeItems() self.__itemModifiedAttributes = ModifiedAttributeDict() - self.__itemModifiedAttributes.original = self.item.attributes + self.__itemModifiedAttributes.original = dict(self.item.attributes) + self.__itemModifiedAttributes.original.update(self.EXTRA_ATTRIBUTES) self.commandBonus = 0 diff --git a/gui/builtinContextMenus/__init__.py b/gui/builtinContextMenus/__init__.py index 7ce9ed894..b9fb3386d 100644 --- a/gui/builtinContextMenus/__init__.py +++ b/gui/builtinContextMenus/__init__.py @@ -17,5 +17,6 @@ __all__ = [ #"changeAffectingSkills", "tacticalMode", "targetResists", - "priceClear" + "priceClear", + "amount", ] diff --git a/gui/builtinContextMenus/amount.py b/gui/builtinContextMenus/amount.py new file mode 100644 index 000000000..a8e00a28f --- /dev/null +++ b/gui/builtinContextMenus/amount.py @@ -0,0 +1,76 @@ +from gui.contextMenu import ContextMenu +from gui.itemStats import ItemStatsDialog +import eos.types +import gui.mainFrame +import service +import gui.globalEvents as GE +import wx + +class ChangeAmount(ContextMenu): + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + def display(self, srcContext, selection): + return srcContext in ("cargoItem","projectedFit") + + def getText(self, itmContext, selection): + print selection + return "Change {0} Quantity".format(itmContext) + + def activate(self, fullContext, selection, i): + srcContext = fullContext[0] + dlg = AmountChanger(self.mainFrame, selection[0], srcContext) + dlg.ShowModal() + dlg.Destroy() + +ChangeAmount.register() + +class AmountChanger(wx.Dialog): + + def __init__(self, parent, thing, context): + wx.Dialog.__init__(self, parent, title="Select Amount", size=wx.Size(220, 60)) + self.thing = thing + self.context = context + + bSizer1 = wx.BoxSizer(wx.HORIZONTAL) + + self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) + + bSizer1.Add(self.input, 1, wx.ALL, 5) + self.input.Bind(wx.EVT_CHAR, self.onChar) + self.input.Bind(wx.EVT_TEXT_ENTER, self.change) + self.button = wx.Button(self, wx.ID_OK, u"Done") + bSizer1.Add(self.button, 0, wx.ALL, 5) + + self.SetSizer(bSizer1) + self.Layout() + self.Centre(wx.BOTH) + self.button.Bind(wx.EVT_BUTTON, self.change) + + def change(self, event): + sFit = service.Fit.getInstance() + mainFrame = gui.mainFrame.MainFrame.getInstance() + fitID = mainFrame.getActiveFit() + + if isinstance(self.thing, eos.types.Cargo): + sFit.addCargo(fitID, self.thing.item.ID, int(self.input.GetLineText(0)), replace=True) + elif isinstance(self.thing, eos.types.Fit): + sFit.changeAmount(fitID, self.thing, int(self.input.GetLineText(0))) + + wx.PostEvent(mainFrame, GE.FitChanged(fitID=fitID)) + + event.Skip() + self.Destroy() + + ## checks to make sure it's valid number + def onChar(self, event): + key = event.GetKeyCode() + + acceptable_characters = "1234567890" + acceptable_keycode = [3, 22, 13, 8, 127] # modifiers like delete, copy, paste + if key in acceptable_keycode or key >= 255 or (key < 255 and chr(key) in acceptable_characters): + event.Skip() + return + else: + return False + diff --git a/gui/builtinContextMenus/cargo.py b/gui/builtinContextMenus/cargo.py index 6eda67458..3cc86902b 100644 --- a/gui/builtinContextMenus/cargo.py +++ b/gui/builtinContextMenus/cargo.py @@ -29,67 +29,3 @@ class Cargo(ContextMenu): wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) Cargo.register() - -class CargoAmount(ContextMenu): - def __init__(self): - self.mainFrame = gui.mainFrame.MainFrame.getInstance() - - def display(self, srcContext, selection): - return srcContext in ("cargoItem",) and selection[0].amount >= 0 - - def getText(self, itmContext, selection): - return "Change {0} Quantity".format(itmContext) - - def activate(self, fullContext, selection, i): - srcContext = fullContext[0] - dlg = CargoChanger(self.mainFrame, selection[0], srcContext) - dlg.ShowModal() - dlg.Destroy() - -CargoAmount.register() - -class CargoChanger(wx.Dialog): - - def __init__(self, parent, cargo, context): - wx.Dialog.__init__(self, parent, title="Select Amount", size=wx.Size(220, 60)) - self.cargo = cargo - self.context = context - - bSizer1 = wx.BoxSizer(wx.HORIZONTAL) - - self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) - - bSizer1.Add(self.input, 1, wx.ALL, 5) - self.input.Bind(wx.EVT_CHAR, self.onChar) - self.input.Bind(wx.EVT_TEXT_ENTER, self.change) - self.button = wx.Button(self, wx.ID_OK, u"Done") - bSizer1.Add(self.button, 0, wx.ALL, 5) - - self.SetSizer(bSizer1) - self.Layout() - self.Centre(wx.BOTH) - self.button.Bind(wx.EVT_BUTTON, self.change) - - def change(self, event): - sFit = service.Fit.getInstance() - mainFrame = gui.mainFrame.MainFrame.getInstance() - fitID = mainFrame.getActiveFit() - - sFit.addCargo(fitID, self.cargo.item.ID, int(self.input.GetLineText(0)), replace=True) - - wx.PostEvent(mainFrame, GE.FitChanged(fitID=fitID)) - - event.Skip() - self.Destroy() - ## checks to make sure it's valid number - def onChar(self, event): - key = event.GetKeyCode() - - acceptable_characters = "1234567890" - acceptable_keycode = [3, 22, 13, 8, 127] # modifiers like delete, copy, paste - if key in acceptable_keycode or key >= 255 or (key < 255 and chr(key) in acceptable_characters): - event.Skip() - return - else: - return False - diff --git a/gui/builtinContextMenus/itemStats.py b/gui/builtinContextMenus/itemStats.py index 47054edc6..73ba66a56 100644 --- a/gui/builtinContextMenus/itemStats.py +++ b/gui/builtinContextMenus/itemStats.py @@ -15,7 +15,8 @@ class ItemStats(ContextMenu): "cargoItem", "droneItem", "implantItem", "boosterItem", "skillItem", "projectedModule", - "projectedDrone", "projectedCharge") + "projectedDrone", "projectedCharge", + "itemStats") def getText(self, itmContext, selection): return "{0} Stats".format(itmContext if itmContext is not None else "Item") diff --git a/gui/builtinViewColumns/baseName.py b/gui/builtinViewColumns/baseName.py index a6b3fe49f..97b992337 100644 --- a/gui/builtinViewColumns/baseName.py +++ b/gui/builtinViewColumns/baseName.py @@ -18,9 +18,9 @@ # along with pyfa. If not, see . #=============================================================================== -from gui import builtinViewColumns from gui.viewColumn import ViewColumn -from gui import bitmapLoader +import gui.mainFrame + import wx from eos.types import Drone, Cargo, Fit, Module, Slot, Rack import service @@ -29,9 +29,12 @@ class BaseName(ViewColumn): name = "Base Name" def __init__(self, fittingView, params): ViewColumn.__init__(self, fittingView) + + self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.columnText = "Name" self.shipImage = fittingView.imageList.GetImageIndex("ship_small", "icons") self.mask = wx.LIST_MASK_TEXT + self.projectedView = isinstance(fittingView, gui.projectedView.ProjectedView) def getText(self, stuff): if isinstance(stuff, Drone): @@ -39,7 +42,12 @@ class BaseName(ViewColumn): elif isinstance(stuff, Cargo): return "%dx %s" % (stuff.amount, stuff.item.name) elif isinstance(stuff, Fit): - return "%s (%s)" % (stuff.name, stuff.ship.item.name) + if self.projectedView: + # we need a little more information for the projected view + fitID = self.mainFrame.getActiveFit() + return "%dx %s (%s)" % (stuff.getProjectionInfo(fitID).amount, stuff.name, stuff.ship.item.name) + else: + return "%s (%s)" % (stuff.name, stuff.ship.item.name) elif isinstance(stuff, Rack): if service.Fit.getInstance().serviceFittingOptions["rackLabels"]: if stuff.slot == Slot.MODE: diff --git a/gui/builtinViewColumns/state.py b/gui/builtinViewColumns/state.py index d26c46443..8110a85ad 100644 --- a/gui/builtinViewColumns/state.py +++ b/gui/builtinViewColumns/state.py @@ -19,27 +19,21 @@ from gui.viewColumn import ViewColumn from gui import bitmapLoader +import gui.mainFrame + import wx -from eos.types import Drone, Module, Rack +from eos.types import Drone, Module, Rack, Fit from eos.types import State as State_ class State(ViewColumn): name = "State" def __init__(self, fittingView, params): ViewColumn.__init__(self, fittingView) + self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.resizable = False self.size = 16 self.maxsize = self.size self.mask = wx.LIST_MASK_IMAGE - for name, state in (("checked", wx.CONTROL_CHECKED), ("unchecked", 0)): - bitmap = wx.EmptyBitmap(16, 16) - dc = wx.MemoryDC() - dc.SelectObject(bitmap) - dc.SetBackground(wx.TheBrushList.FindOrCreateBrush(fittingView.GetBackgroundColour(), wx.SOLID)) - dc.Clear() - wx.RendererNative.Get().DrawCheckBox(fittingView, dc, wx.Rect(0, 0, 16, 16), state) - dc.Destroy() - setattr(self, "%sId" % name, fittingView.imageList.Add(bitmap)) def getText(self, mod): return "" @@ -49,8 +43,14 @@ class State(ViewColumn): return State_.getName(mod.state).title() def getImageId(self, stuff): + generic_active = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(1).lower(), "icons") + generic_inactive = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(-1).lower(), "icons") + if isinstance(stuff, Drone): - return self.checkedId if stuff.amountActive > 0 else self.uncheckedId + if stuff.amountActive > 0: + return generic_active + else: + return generic_inactive elif isinstance(stuff, Rack): return -1 elif isinstance(stuff, Module): @@ -58,11 +58,21 @@ class State(ViewColumn): return -1 else: return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(stuff.state).lower(), "icons") + elif isinstance(stuff, Fit): + fitID = self.mainFrame.getActiveFit() + projectionInfo = stuff.getProjectionInfo(fitID) + + if projectionInfo is None: + return -1 + if projectionInfo.active: + return generic_active + return generic_inactive else: active = getattr(stuff, "active", None) if active is None: return -1 - else: - return self.checkedId if active else self.uncheckedId + if active: + return generic_active + return generic_inactive State.register() diff --git a/gui/itemStats.py b/gui/itemStats.py index a26406af5..9d971aaa7 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -24,10 +24,11 @@ import bitmapLoader import sys import wx.lib.mixins.listctrl as listmix import wx.html -from eos.types import Ship, Module, Skill, Booster, Implant, Drone, Mode +from eos.types import Fit, Ship, Module, Skill, Booster, Implant, Drone, Mode from gui.utils.numberFormatter import formatAmount import service import config +from gui.contextMenu import ContextMenu try: from collections import OrderedDict @@ -549,15 +550,20 @@ class ItemEffects (wx.Panel): class ItemAffectedBy (wx.Panel): - ORDER = [Ship, Mode, Module, Drone, Implant, Booster, Skill] + ORDER = [Fit, Ship, Mode, Module, Drone, Implant, Booster, Skill] def __init__(self, parent, stuff, item): - wx.Panel.__init__ (self, parent) + wx.Panel.__init__(self, parent) self.stuff = stuff self.item = item - self.toggleView = 1 + self.activeFit = gui.mainFrame.MainFrame.getInstance().getActiveFit() + + self.showRealNames = False + self.showAttrView = False self.expand = -1 + self.treeItems = [] + mainSizer = wx.BoxSizer(wx.VERTICAL) self.affectedBy = wx.TreeCtrl(self, style = wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.NO_BORDER) @@ -571,7 +577,10 @@ class ItemAffectedBy (wx.Panel): self.toggleExpandBtn = wx.ToggleButton( self, wx.ID_ANY, u"Expand All", wx.DefaultPosition, wx.DefaultSize, 0 ) bSizer.Add( self.toggleExpandBtn, 0, wx.ALIGN_CENTER_VERTICAL) - self.toggleViewBtn = wx.ToggleButton( self, wx.ID_ANY, u"Toggle view mode", wx.DefaultPosition, wx.DefaultSize, 0 ) + self.toggleNameBtn = wx.ToggleButton( self, wx.ID_ANY, u"Toggle Names", wx.DefaultPosition, wx.DefaultSize, 0 ) + bSizer.Add( self.toggleNameBtn, 0, wx.ALIGN_CENTER_VERTICAL) + + self.toggleViewBtn = wx.ToggleButton( self, wx.ID_ANY, u"Toggle View", wx.DefaultPosition, wx.DefaultSize, 0 ) bSizer.Add( self.toggleViewBtn, 0, wx.ALIGN_CENTER_VERTICAL) if stuff is not None: @@ -579,13 +588,36 @@ class ItemAffectedBy (wx.Panel): bSizer.Add( self.refreshBtn, 0, wx.ALIGN_CENTER_VERTICAL) self.refreshBtn.Bind( wx.EVT_BUTTON, self.RefreshTree ) - self.toggleViewBtn.Bind(wx.EVT_TOGGLEBUTTON,self.ToggleViewMode) + self.toggleNameBtn.Bind(wx.EVT_TOGGLEBUTTON,self.ToggleNameMode) self.toggleExpandBtn.Bind(wx.EVT_TOGGLEBUTTON,self.ToggleExpand) + self.toggleViewBtn.Bind(wx.EVT_TOGGLEBUTTON,self.ToggleViewMode) mainSizer.Add( bSizer, 0, wx.ALIGN_RIGHT) self.SetSizer(mainSizer) self.PopulateTree() self.Layout() + self.affectedBy.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.scheduleMenu) + + def scheduleMenu(self, event): + event.Skip() + wx.CallAfter(self.spawnMenu, event.Item) + + def spawnMenu(self, item): + self.affectedBy.SelectItem(item) + + stuff = self.affectedBy.GetPyData(item) + # String is set as data when we are dealing with attributes, not stuff containers + if stuff is None or isinstance(stuff, basestring): + return + contexts = [] + + # Skills are different in that they don't have itemModifiedAttributes, + # which is needed if we send the container to itemStats dialog. So + # instead, we send the item. + type = stuff.__class__.__name__ + contexts.append(("itemStats", type)) + menu = ContextMenu.getMenu(stuff if type != "Skill" else stuff.item, *contexts) + self.PopupMenu(menu) def ExpandCollapseTree(self): @@ -607,18 +639,11 @@ class ItemAffectedBy (wx.Panel): def ToggleViewTree(self): self.Freeze() - root = self.affectedBy.GetRootItem() - child,cookie = self.affectedBy.GetFirstChild(root) - while child.IsOk(): - item,childcookie = self.affectedBy.GetFirstChild(child) - while item.IsOk(): - change = self.affectedBy.GetPyData(item) - display = self.affectedBy.GetItemText(item) - self.affectedBy.SetItemText(item,change) - self.affectedBy.SetPyData(item,display) - item,childcookie = self.affectedBy.GetNextChild(child,childcookie) - - child,cookie = self.affectedBy.GetNextChild(root,cookie) + for item in self.treeItems: + change = self.affectedBy.GetPyData(item) + display = self.affectedBy.GetItemText(item) + self.affectedBy.SetItemText(item, change) + self.affectedBy.SetPyData(item, display) self.Thaw() @@ -633,33 +658,214 @@ class ItemAffectedBy (wx.Panel): event.Skip() def ToggleViewMode(self, event): - self.toggleView *=-1 + self.showAttrView = not self.showAttrView + self.affectedBy.DeleteAllItems() + self.PopulateTree() + event.Skip() + + def ToggleNameMode(self, event): + self.showRealNames = not self.showRealNames self.ToggleViewTree() event.Skip() def PopulateTree(self): + # sheri was here + del self.treeItems[:] root = self.affectedBy.AddRoot("WINPWNZ0R") self.affectedBy.SetPyData(root, None) self.imageList = wx.ImageList(16, 16) self.affectedBy.SetImageList(self.imageList) - cont = self.stuff.itemModifiedAttributes if self.item == self.stuff.item else self.stuff.chargeModifiedAttributes - things = {} + if self.showAttrView: + self.buildAttributeView(root) + else: + self.buildModuleView(root) - for attrName in cont.iterAfflictions(): + self.ExpandCollapseTree() + + def sortAttrDisplayName(self, attr): + info = self.stuff.item.attributes.get(attr) + if info and info.displayName != "": + return info.displayName + + return attr + + def buildAttributeView(self, root): + # We first build a usable dictionary of items. The key is either a fit + # if the afflictions stem from a projected fit, or self.stuff if they + # are local afflictions (everything else, even gang boosts at this time) + # The value of this is yet another dictionary in the following format: + # + # "attribute name": { + # "Module Name": [ + # class of affliction, + # affliction item (required due to GH issue #335) + # modifier type + # amount of modification + # whether this affliction was projected + # ] + # } + + attributes = self.stuff.itemModifiedAttributes if self.item == self.stuff.item else self.stuff.chargeModifiedAttributes + container = {} + for attrName in attributes.iterAfflictions(): # if value is 0 or there has been no change from original to modified, return - if cont[attrName] == (cont.getOriginal(attrName) or 0): + if attributes[attrName] == (attributes.getOriginal(attrName) or 0): continue - for fit, afflictors in cont.getAfflictions(attrName).iteritems(): + + for fit, afflictors in attributes.getAfflictions(attrName).iteritems(): for afflictor, modifier, amount, used in afflictors: + if not used or afflictor.item is None: continue - if afflictor.item.name not in things: - things[afflictor.item.name] = [type(afflictor), set(), []] + if fit.ID != self.activeFit: + # affliction fit does not match our fit + if fit not in container: + container[fit] = {} + items = container[fit] + else: + # local afflictions + if self.stuff not in container: + container[self.stuff] = {} + items = container[self.stuff] - info = things[afflictor.item.name] + # items hold our module: info mappings + if attrName not in items: + items[attrName] = [] + + if afflictor == self.stuff and getattr(afflictor, 'charge', None): + # we are showing a charges modifications, see #335 + item = afflictor.charge + else: + item = afflictor.item + + items[attrName].append((type(afflictor), afflictor, item, modifier, amount, getattr(afflictor, "projected", False))) + + # Make sure projected fits are on top + rootOrder = container.keys() + rootOrder.sort(key=lambda x: self.ORDER.index(type(x))) + + # Now, we take our created dictionary and start adding stuff to our tree + for thing in rootOrder: + # This block simply directs which parent we are adding to (root or projected fit) + if thing == self.stuff: + parent = root + else: # projected fit + icon = self.imageList.Add(bitmapLoader.getBitmap("ship_small", "icons")) + child = self.affectedBy.AppendItem(root, "{} ({})".format(thing.name, thing.ship.item.name), icon) + parent = child + + attributes = container[thing] + attrOrder = sorted(attributes.keys(), key=self.sortAttrDisplayName) + + for attrName in attrOrder: + attrInfo = self.stuff.item.attributes.get(attrName) + displayName = attrInfo.displayName if attrInfo and attrInfo.displayName != "" else attrName + + if attrInfo: + if attrInfo.icon is not None: + iconFile = attrInfo.icon.iconFile + icon = bitmapLoader.getBitmap(iconFile, "pack") + if icon is None: + icon = bitmapLoader.getBitmap("transparent16x16", "icons") + attrIcon = self.imageList.Add(icon) + else: + attrIcon = self.imageList.Add(bitmapLoader.getBitmap("07_15", "pack")) + else: + attrIcon = self.imageList.Add(bitmapLoader.getBitmap("07_15", "pack")) + + if self.showRealNames: + display = attrName + saved = displayName + else: + display = displayName + saved = attrName + + # this is the attribute node + child = self.affectedBy.AppendItem(parent, display, attrIcon) + self.affectedBy.SetPyData(child, saved) + self.treeItems.append(child) + + items = attributes[attrName] + items.sort(key=lambda x: self.ORDER.index(x[0])) + for itemInfo in items: + afflictorType, afflictor, item, attrModifier, attrAmount, projected = itemInfo + + if afflictorType == Ship: + itemIcon = self.imageList.Add(bitmapLoader.getBitmap("ship_small", "icons")) + elif item.icon: + bitmap = bitmapLoader.getBitmap(item.icon.iconFile, "pack") + itemIcon = self.imageList.Add(bitmap) if bitmap else -1 + else: + itemIcon = -1 + + displayStr = item.name + + if projected: + displayStr += " (projected)" + + if attrModifier == "s*": + attrModifier = "*" + penalized = "(penalized)" + else: + penalized = "" + + # this is the Module node, the attribute will be attached to this + display = "%s %s %.2f %s" % (displayStr, attrModifier, attrAmount, penalized) + treeItem = self.affectedBy.AppendItem(child, display, itemIcon) + self.affectedBy.SetPyData(treeItem, afflictor) + + + def buildModuleView(self, root): + # We first build a usable dictionary of items. The key is either a fit + # if the afflictions stem from a projected fit, or self.stuff if they + # are local afflictions (everything else, even gang boosts at this time) + # The value of this is yet another dictionary in the following format: + # + # "Module Name": [ + # class of affliction, + # set of afflictors (such as 2 of the same module), + # info on affliction (attribute name, modifier, and modification amount), + # item that will be used to determine icon (required due to GH issue #335) + # whether this affliction is actually used (unlearned skills are not used) + # ] + + attributes = self.stuff.itemModifiedAttributes if self.item == self.stuff.item else self.stuff.chargeModifiedAttributes + container = {} + for attrName in attributes.iterAfflictions(): + # if value is 0 or there has been no change from original to modified, return + if attributes[attrName] == (attributes.getOriginal(attrName) or 0): + continue + + for fit, afflictors in attributes.getAfflictions(attrName).iteritems(): + for afflictor, modifier, amount, used in afflictors: + if not used or getattr(afflictor, 'item', None) is None: + continue + + if fit.ID != self.activeFit: + # affliction fit does not match our fit + if fit not in container: + container[fit] = {} + items = container[fit] + else: + # local afflictions + if self.stuff not in container: + container[self.stuff] = {} + items = container[self.stuff] + + if afflictor == self.stuff and getattr(afflictor, 'charge', None): + # we are showing a charges modifications, see #335 + item = afflictor.charge + else: + item = afflictor.item + + # items hold our module: info mappings + if item.name not in items: + items[item.name] = [type(afflictor), set(), [], item, getattr(afflictor, "projected", False)] + + info = items[item.name] info[1].add(afflictor) # If info[1] > 1, there are two separate modules working. # Check to make sure we only include the modifier once @@ -668,63 +874,86 @@ class ItemAffectedBy (wx.Panel): continue info[2].append((attrName, modifier, amount)) - order = things.keys() - order.sort(key=lambda x: (self.ORDER.index(things[x][0]), x)) + # Make sure projected fits are on top + rootOrder = container.keys() + rootOrder.sort(key=lambda x: self.ORDER.index(type(x))) - for itemName in order: - info = things[itemName] + # Now, we take our created dictionary and start adding stuff to our tree + for thing in rootOrder: + # This block simply directs which parent we are adding to (root or projected fit) + if thing == self.stuff: + parent = root + else: # projected fit + icon = self.imageList.Add(bitmapLoader.getBitmap("ship_small", "icons")) + child = self.affectedBy.AppendItem(root, "{} ({})".format(thing.name, thing.ship.item.name), icon) + parent = child - afflictorType, afflictors, attrData = info - counter = len(afflictors) + items = container[thing] + order = items.keys() + order.sort(key=lambda x: (self.ORDER.index(items[x][0]), x)) - baseAfflictor = afflictors.pop() - if afflictorType == Ship: - itemIcon = self.imageList.Add(bitmapLoader.getBitmap("ship_small", "icons")) - elif baseAfflictor.item.icon: - bitmap = bitmapLoader.getBitmap(baseAfflictor.item.icon.iconFile, "pack") - itemIcon = self.imageList.Add(bitmap) if bitmap else -1 - else: - itemIcon = -1 + for itemName in order: + info = items[itemName] + afflictorType, afflictors, attrData, item, projected = info + counter = len(afflictors) + if afflictorType == Ship: + itemIcon = self.imageList.Add(bitmapLoader.getBitmap("ship_small", "icons")) + elif item.icon: + bitmap = bitmapLoader.getBitmap(item.icon.iconFile, "pack") + itemIcon = self.imageList.Add(bitmap) if bitmap else -1 + else: + itemIcon = -1 - child = self.affectedBy.AppendItem(root, "%s" % itemName if counter == 1 else "%s x %d" % (itemName,counter), itemIcon) + displayStr = itemName - if counter > 0: - attributes = [] - for attrName, attrModifier, attrAmount in attrData: - attrInfo = self.stuff.item.attributes.get(attrName) - displayName = attrInfo.displayName if attrInfo else "" + if counter > 1: + displayStr += " x {}".format(counter) - if attrInfo: - if attrInfo.icon is not None: - iconFile = attrInfo.icon.iconFile - icon = bitmapLoader.getBitmap(iconFile, "pack") - if icon is None: - icon = bitmapLoader.getBitmap("transparent16x16", "icons") + if projected: + displayStr += " (projected)" - attrIcon = self.imageList.Add(icon) + # this is the Module node, the attribute will be attached to this + child = self.affectedBy.AppendItem(parent, displayStr, itemIcon) + self.affectedBy.SetPyData(child, afflictors.pop()) + + if counter > 0: + attributes = [] + for attrName, attrModifier, attrAmount in attrData: + attrInfo = self.stuff.item.attributes.get(attrName) + displayName = attrInfo.displayName if attrInfo else "" + + if attrInfo: + if attrInfo.icon is not None: + iconFile = attrInfo.icon.iconFile + icon = bitmapLoader.getBitmap(iconFile, "pack") + if icon is None: + icon = bitmapLoader.getBitmap("transparent16x16", "icons") + + attrIcon = self.imageList.Add(icon) + else: + attrIcon = self.imageList.Add(bitmapLoader.getBitmap("07_15", "pack")) else: attrIcon = self.imageList.Add(bitmapLoader.getBitmap("07_15", "pack")) - else: - attrIcon = self.imageList.Add(bitmapLoader.getBitmap("07_15", "pack")) - if attrModifier == "s*": - attrModifier = "*" - penalized = "(penalized)" - else: - penalized = "" + if attrModifier == "s*": + attrModifier = "*" + penalized = "(penalized)" + else: + penalized = "" - attributes.append((attrName, (displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized, attrIcon)) + attributes.append((attrName, (displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized, attrIcon)) - attrSorted = sorted(attributes, key = lambda attribName: attribName[0]) + attrSorted = sorted(attributes, key = lambda attribName: attribName[0]) + for attr in attrSorted: + attrName, displayName, attrModifier, attrAmount, penalized, attrIcon = attr - for attr in attrSorted: - attrName, displayName, attrModifier, attrAmount, penalized, attrIcon = attr - if self.toggleView == 1: - treeitem = self.affectedBy.AppendItem(child, "%s %s %.2f %s" % ((displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized), attrIcon) - self.affectedBy.SetPyData(treeitem,"%s %s %.2f %s" % (attrName, attrModifier, attrAmount, penalized)) - else: - treeitem = self.affectedBy.AppendItem(child, "%s %s %.2f %s" % (attrName, attrModifier, attrAmount, penalized), attrIcon) - self.affectedBy.SetPyData(treeitem,"%s %s %.2f %s" % ((displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized)) - - self.ExpandCollapseTree() + if self.showRealNames: + display = "%s %s %.2f %s" % (attrName, attrModifier, attrAmount, penalized) + saved = "%s %s %.2f %s" % ((displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized) + else: + display = "%s %s %.2f %s" % ((displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized) + saved = "%s %s %.2f %s" % (attrName, attrModifier, attrAmount, penalized) + treeitem = self.affectedBy.AppendItem(child, display, attrIcon) + self.affectedBy.SetPyData(treeitem, saved) + self.treeItems.append(treeitem) diff --git a/service/fit.py b/service/fit.py index f20d9183a..e848b45d7 100644 --- a/service/fit.py +++ b/service/fit.py @@ -36,7 +36,7 @@ from service.fleet import Fleet from service.settings import SettingsProvider from service.port import Port -logger = logging.getLogger("pyfa.service.fit") +logger = logging.getLogger(__name__) class FitBackupThread(threading.Thread): def __init__(self, path, callback): @@ -175,10 +175,14 @@ class Fit(object): fit = eos.db.getFit(fitID) sFleet = Fleet.getInstance() sFleet.removeAssociatedFleetData(fit) - self.removeProjectedData(fitID) eos.db.remove(fit) + # refresh any fits this fit is projected onto. Otherwise, if we have + # already loaded those fits, they will not reflect the changes + for projection in fit.projectedOnto.values(): + eos.db.saveddata_session.refresh(projection.victim_fit) + def copyFit(self, fitID): fit = eos.db.getFit(fitID) newFit = copy.deepcopy(fit) @@ -193,14 +197,6 @@ class Fit(object): fit.clear() return fit - def removeProjectedData(self, fitID): - """Removes projection relation from ships that have fitID as projection. See GitHub issue #90""" - fit = eos.db.getFit(fitID) - fits = eos.db.getProjectedFits(fitID) - - for projectee in fits: - projectee.projectedFits.remove(fit) - def toggleFactorReload(self, fitID): if fitID is None: return None @@ -236,6 +232,7 @@ class Fit(object): return None fit = eos.db.getFit(fitID) inited = getattr(fit, "inited", None) + if inited is None or inited is False: sFleet = Fleet.getInstance() f = sFleet.getLinearFleet(fit) @@ -246,6 +243,7 @@ class Fit(object): fit.fleet = f if not projected: + print "Not projected, getting projected fits" for fitP in fit.projectedFits: self.getFit(fitP.ID, projected = True) self.recalc(fit, withBoosters=True) @@ -322,9 +320,14 @@ class Fit(object): eager=("attributes", "group.category")) if isinstance(thing, eos.types.Fit): - if thing.ID == fitID: + if thing in fit.projectedFits: return - fit.projectedFits.append(thing) + + fit.__projectedFits[thing.ID] = thing + + # this bit is required -- see GH issue # 83 + eos.db.saveddata_session.flush() + eos.db.saveddata_session.refresh(thing) elif thing.category.name == "Drone": drone = None for d in fit.projectedDrones.find(thing): @@ -363,6 +366,22 @@ class Fit(object): thing.state = self.__getProposedState(thing, click) if not thing.canHaveState(thing.state, fit): thing.state = State.OFFLINE + elif isinstance(thing, eos.types.Fit): + print "toggle fit" + projectionInfo = thing.getProjectionInfo(fitID) + if projectionInfo: + projectionInfo.active = not projectionInfo.active + + eos.db.commit() + self.recalc(fit) + + def changeAmount(self, fitID, projected_fit, amount): + """Change amount of projected fits""" + fit = eos.db.getFit(fitID) + amount = min(20, max(1, amount)) # 1 <= a <= 20 + projectionInfo = projected_fit.getProjectionInfo(fitID) + if projectionInfo: + projectionInfo.amount = amount eos.db.commit() self.recalc(fit) @@ -374,7 +393,8 @@ class Fit(object): elif isinstance(thing, eos.types.Module): fit.projectedModules.remove(thing) else: - fit.projectedFits.remove(thing) + del fit.__projectedFits[thing.ID] + #fit.projectedFits.remove(thing) eos.db.commit() self.recalc(fit) @@ -921,8 +941,9 @@ class Fit(object): eos.db.commit() self.recalc(fit) - def recalc(self, fit, withBoosters=False): + def recalc(self, fit, withBoosters=True): + logger.debug("="*10+"recalc"+"="*10) if fit.factorReload is not self.serviceFittingOptions["useGlobalForceReload"]: fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"] - fit.clear() - fit.calculateModifiedAttributes(withBoosters=withBoosters, dirtyStorage=self.dirtyFitIDs) + fit.clear() + fit.calculateModifiedAttributes(withBoosters=withBoosters) diff --git a/utils/timer.py b/utils/timer.py index 811dfe348..da355cba9 100644 --- a/utils/timer.py +++ b/utils/timer.py @@ -1,30 +1,36 @@ import time -class Timer(object): - """ - Generic timing class for simple profiling. +class Timer(): + def __init__(self, name='', logger=None): + self.name = name + self.start = time.time() + self.__last = self.start + self.logger = logger - Usage: + @property + def elapsed(self): + return (time.time() - self.start)*1000 - with Timer(verbose=True) as t: - # code to be timed - time.sleep(5) + @property + def last(self): + return (time.time() - self.__last)*1000 - Output: - elapsed time: 5000.000 ms - - Can also access time with t.secs - """ - def __init__(self, verbose=False): - self.verbose = verbose + def checkpoint(self, name=''): + text = 'Timer - {timer} - {checkpoint} - {last:.2f}ms ({elapsed:.2f}ms elapsed)'.format( + timer=self.name, + checkpoint=name, + last=self.last, + elapsed=self.elapsed + ).strip() + self.__last = time.time() + if self.logger: + self.logger.debug(text) + else: + print text def __enter__(self): - self.start = time.time() return self - def __exit__(self, *args): - self.end = time.time() - self.secs = self.end - self.start - self.msecs = self.secs * 1000 # millisecs - if self.verbose: - print 'elapsed time: %f ms' % self.msecs \ No newline at end of file + def __exit__(self, type, value, traceback): + self.checkpoint('finished') + pass