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