Compare commits

...

31 Commits

Author SHA1 Message Date
blitzmann
c6e0604c69 Merge branch 'regSkills' into preview_proj_skills 2015-07-20 15:28:16 -04:00
blitzmann
4596c526a2 Fix #335 - properly represent charge modifiers 2015-07-18 23:39:16 -04:00
blitzmann
9941b6c74b Make default view module again 2015-07-18 15:23:10 -04:00
blitzmann
b06ce24d4a Fix attribute view items (two of the same item would merge into one, even if they had different modifiers. now simply list them individually) 2015-07-18 15:20:15 -04:00
blitzmann
2f8c201ab3 Add attribute view 2015-07-18 14:45:26 -04:00
blitzmann
d184820728 Fix toggle attribute names. Previous way assumed no other trees apart from first child and siblings. Not true now with projected fit trees. Instead of attempting to walk the tree, we simply store the items in a list that we later iterate over. Much easier 2015-07-17 18:28:31 -04:00
blitzmann
40aeb1ed4a Move active fir to init, fixes bug when refreshing with another active fit 2015-07-17 16:33:07 -04:00
blitzmann
71b258a8f5 Merge fit attributes with ship 2015-07-17 16:32:42 -04:00
blitzmann
d6199a58c2 Separate projected fits from list of affectors. Also, show when affected module is projected. Still need to clean up affector tree stuff 2015-07-16 23:59:37 -04:00
blitzmann
3ad5aaac89 Fix #331 - gang boosts not applied to self projection 2015-07-15 16:52:09 -04:00
blitzmann
3bed268d81 Fix use case for downgrading and adding a row with NULL 2015-07-14 19:13:56 -04:00
blitzmann
9a1b0f07c0 Added documentation on why projections don't respect the __calculated flag which gang boosts do 2015-07-14 16:32:10 -04:00
blitzmann
f591ecba10 Fix use case where gang boosts were not being applied when projections were added/removed. 2015-07-14 16:15:52 -04:00
blitzmann
c571fdc5e6 Fit fit alterations with self projections 2015-07-13 19:29:23 -04:00
blitzmann
63fce4be17 Handle self projections by creating a copy of the fit. Due to the way effects are calculated, we would have double effects implemented if not for the copy 2015-07-11 16:28:09 -04:00
blitzmann
86ee5292d8 Fix fit copying and deleting fits not being reflected in other fits. 2015-07-11 12:43:47 -04:00
blitzmann
23b458534f Remove unneeded collection class for projected fits 2015-07-10 23:22:58 -04:00
blitzmann
2256efacb0 Do migration stuff for projected fits 2015-07-10 16:40:00 -04:00
blitzmann
28a5318e3b Merge branch 'master' into toggleProjectionFit
Conflicts:
	config.py
2015-07-10 16:09:00 -04:00
blitzmann
609ee13cd6 Redirect stderr and stdout to logger when we are frozen. Need to test this. 2015-07-10 15:58:45 -04:00
blitzmann
4216904736 Remove function to remove projected fits correctly. This is now handled by proper DB relationships 2015-07-10 15:46:42 -04:00
blitzmann
496e9b56b5 Handle use case of invalid fit's mucking things up 2015-07-10 15:46:15 -04:00
blitzmann
68dddf2810 Fix for init projected fit. Took a long time to figure out what was happening. 2015-07-10 11:58:15 -04:00
blitzmann
c17e03d8d0 Fixes critical design issue when it comes to projected fits. Disabled some of the more advanced functionality (projection amount and active) to cope to development. Crash still happens occasionally when adding projected fit for unknown reasons - not 100% reproducable yet 2015-07-09 17:53:41 -04:00
blitzmann
af9f64db5f Move the chain into the runtime loop, otherwise projections won't work for some odd reason. 2015-07-09 14:48:15 -04:00
blitzmann
c17bce55bb Lots of stuff
- Added logging and replaced Timer class with more useful one
- Move projected fit loop out of runtimes
- Eliminate recursions from Fit.clear()
- Clean up overall fit calc logic
2015-07-09 10:43:39 -04:00
blitzmann
23309a5da6 Remove unneeded code that created a bitmap for checkboxes 2015-07-07 13:49:40 -04:00
blitzmann
06e4a7e80f Support changing amount of projected fits 2015-07-07 13:49:39 -04:00
blitzmann
b95a10d284 Add active column. Looping the fit to apply it x amount of times doesn't seem to work. Probably because it's been flagged calculated and returns early 2015-07-07 13:49:38 -04:00
blitzmann
2bca3ddcc8 GUI support (also made regular checkboxes pretty for drones/implant/etc) 2015-07-07 13:49:37 -04:00
blitzmann
9ef182aa99 First working prototype of toggleable projected fits. Creates a new association object that stores projection-specific information. GUI hasn't been touched (need to show state), and there are a lot of variables that I need to rename. 2015-07-07 13:49:35 -04:00
17 changed files with 784 additions and 309 deletions

View File

@@ -17,7 +17,7 @@ debug = False
# Defines if our saveddata will be in pyfa root or not # Defines if our saveddata will be in pyfa root or not
saveInRoot = False saveInRoot = False
logLevel = logging.WARN logLevel = logging.DEBUG
# Version data # Version data
version = "1.13.3" version = "1.13.3"
@@ -32,6 +32,21 @@ staticPath = None
saveDB = None saveDB = None
gameDB = 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): def __createDirs(path):
if not os.path.exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
@@ -72,15 +87,14 @@ def defPaths():
logging.info("Starting pyfa") logging.info("Starting pyfa")
# Redirect stderr to file if we're requested to do so if hasattr(sys, 'frozen'):
stderrToFile = getattr(configforced, "stderrToFile", None) stdout_logger = logging.getLogger('STDOUT')
if stderrToFile is True: sl = StreamToLogger(stdout_logger, logging.INFO)
sys.stderr = open(os.path.join(savePath, "error_log.txt"), "w") sys.stdout = sl
# Same for stdout stderr_logger = logging.getLogger('STDERR')
stdoutToFile = getattr(configforced, "stdoutToFile", None) sl = StreamToLogger(stderr_logger, logging.ERROR)
if stdoutToFile is True: sys.stderr = sl
sys.stdout = open(os.path.join(savePath, "output_log.txt"), "w")
# Static EVE Data from the staticdata repository, should be in the staticdata # Static EVE Data from the staticdata repository, should be in the staticdata
# directory in our pyfa directory # directory in our pyfa directory

View File

@@ -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")

View File

@@ -17,9 +17,11 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>. # along with eos. If not, see <http://www.gnu.org/licenses/>.
#=============================================================================== #===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, String, Boolean from sqlalchemy import *
from sqlalchemy.orm import relation, mapper from sqlalchemy.orm import *
from sqlalchemy.sql import and_ 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 import saveddata_meta
from eos.db.saveddata.module import modules_table from eos.db.saveddata.module import modules_table
@@ -45,33 +47,119 @@ fits_table = Table("fits", saveddata_meta,
projectedFits_table = Table("projectedFits", saveddata_meta, projectedFits_table = Table("projectedFits", saveddata_meta,
Column("sourceID", ForeignKey("fits.ID"), primary_key = True), Column("sourceID", ForeignKey("fits.ID"), primary_key = True),
Column("victimID", 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, mapper(Fit, fits_table,
properties = {"_Fit__modules" : relation(Module, collection_class = HandledModuleList, properties = {
primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False), "_Fit__modules": relation(
order_by = modules_table.c.position, cascade='all, delete, delete-orphan'), Module,
"_Fit__projectedModules" : relation(Module, collection_class = HandledProjectedModList, cascade='all, delete, delete-orphan', single_parent=True, collection_class=HandledModuleList,
primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)), primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False),
"owner" : relation(User, backref = "fits"), order_by=modules_table.c.position,
"itemID" : fits_table.c.shipID, cascade='all, delete, delete-orphan'),
"shipID" : fits_table.c.shipID, "_Fit__projectedModules": relation(
"_Fit__boosters" : relation(Booster, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', single_parent=True), Module,
"_Fit__drones" : relation(Drone, collection_class = HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, collection_class=HandledProjectedModList,
primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)), cascade='all, delete, delete-orphan',
"_Fit__cargo" : relation(Cargo, collection_class = HandledDroneCargoList, cascade='all, delete, delete-orphan', single_parent=True, single_parent=True,
primaryjoin = and_(cargo_table.c.fitID == fits_table.c.ID)), primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)),
"_Fit__projectedDrones" : relation(Drone, collection_class = HandledProjectedDroneList, cascade='all, delete, delete-orphan', single_parent=True, "owner": relation(
primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == True)), User,
"_Fit__implants" : relation(Implant, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', backref='fit', single_parent=True, backref="fits"),
primaryjoin = fitImplants_table.c.fitID == fits_table.c.ID, "itemID": fits_table.c.shipID,
secondaryjoin = fitImplants_table.c.implantID == Implant.ID, "shipID": fits_table.c.shipID,
secondary = fitImplants_table), "_Fit__boosters": relation(
"_Fit__character" : relation(Character, backref = "fits"), Booster,
"_Fit__damagePattern" : relation(DamagePattern), collection_class=HandledImplantBoosterList,
"_Fit__targetResists" : relation(TargetResists), cascade='all, delete, delete-orphan',
"_Fit__projectedFits" : relation(Fit, single_parent=True),
primaryjoin = projectedFits_table.c.victimID == fits_table.c.ID, "_Fit__drones": relation(
secondaryjoin = fits_table.c.ID == projectedFits_table.c.sourceID, Drone,
secondary = projectedFits_table, collection_class=HandledDroneCargoList,
collection_class = HandledProjectedFitList) 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,
}
)

View File

@@ -232,11 +232,6 @@ class HandledProjectedDroneList(HandledDroneCargoList):
if proj.isInvalid or not proj.item.isType("projected"): if proj.isInvalid or not proj.item.isType("projected"):
self.remove(proj) self.remove(proj)
class HandledProjectedFitList(HandledList):
def append(self, proj):
proj.projected = True
list.append(self, proj)
class HandledItem(object): class HandledItem(object):
def preAssignItemAttr(self, *args, **kwargs): def preAssignItemAttr(self, *args, **kwargs):
self.itemModifiedAttributes.preAssign(*args, **kwargs) self.itemModifiedAttributes.preAssign(*args, **kwargs)

View File

@@ -229,13 +229,16 @@ class ModifiedAttributeDict(collections.MutableMapping):
if attributeName not in self.__affectedBy: if attributeName not in self.__affectedBy:
self.__affectedBy[attributeName] = {} self.__affectedBy[attributeName] = {}
affs = 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 there's no set for current fit in dictionary, create it
if self.fit not in affs: if fit not in affs:
affs[self.fit] = [] affs[fit] = []
# Reassign alias to list # Reassign alias to list
affs = affs[self.fit] affs = affs[fit]
# Get modifier which helps to compose 'Affected by' map # Get modifier which helps to compose 'Affected by' map
modifier = self.fit.getModifier() modifier = self.fit.getModifier()
# Add current affliction to list # Add current affliction to list
affs.append((modifier, operation, bonus, used)) affs.append((modifier, operation, bonus, used))

View File

@@ -286,5 +286,10 @@ class Skill(HandledItem):
copy = Skill(self.item, self.level, self.__ro) copy = Skill(self.item, self.level, self.__ro)
return copy return copy
def __repr__(self):
return "Skill(ID={}, name={}) at {}".format(
self.item.ID, self.item.name, hex(id(self))
)
class ReadOnlyException(Exception): class ReadOnlyException(Exception):
pass pass

View File

@@ -29,6 +29,9 @@ from eos.saveddata.module import State
from eos.saveddata.mode import Mode from eos.saveddata.mode import Mode
import eos.db import eos.db
import time import time
import copy
from utils.timer import Timer
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -40,14 +43,6 @@ except ImportError:
class Fit(object): class Fit(object):
"""Represents a fitting, with modules, ship, implants, etc.""" """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 PEAK_RECHARGE = 0.25
@@ -61,7 +56,7 @@ class Fit(object):
self.__cargo = HandledDroneCargoList() self.__cargo = HandledDroneCargoList()
self.__implants = HandledImplantBoosterList() self.__implants = HandledImplantBoosterList()
self.__boosters = HandledImplantBoosterList() self.__boosters = HandledImplantBoosterList()
self.__projectedFits = HandledProjectedFitList() #self.__projectedFits = {}
self.__projectedModules = HandledProjectedModList() self.__projectedModules = HandledProjectedModList()
self.__projectedDrones = HandledProjectedDroneList() self.__projectedDrones = HandledProjectedDroneList()
self.__character = None self.__character = None
@@ -124,8 +119,7 @@ class Fit(object):
self.boostsFits = set() self.boostsFits = set()
self.gangBoosts = None self.gangBoosts = None
self.ecmProjectedStr = 1 self.ecmProjectedStr = 1
self.extraAttributes = ModifiedAttributeDict(self) self.extraAttributes = self.ship.itemModifiedAttributes
self.extraAttributes.original = self.EXTRA_ATTRIBUTES
@property @property
def targetResists(self): def targetResists(self):
@@ -207,7 +201,12 @@ class Fit(object):
@property @property
def projectedFits(self): 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 @property
def projectedDrones(self): def projectedDrones(self):
@@ -326,7 +325,7 @@ class Fit(object):
if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key) if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key)
else: return val else: return val
def clear(self): def clear(self, projected=False):
self.__effectiveTank = None self.__effectiveTank = None
self.__weaponDPS = None self.__weaponDPS = None
self.__minerYield = None self.__minerYield = None
@@ -346,15 +345,36 @@ class Fit(object):
del self.__calculatedTargets[:] del self.__calculatedTargets[:]
del self.__extraDrains[:] del self.__extraDrains[:]
if self.ship is not None: self.ship.clear() if self.ship:
c = chain(self.modules, self.drones, self.boosters, self.implants, self.projectedDrones, self.projectedModules, self.projectedFits, (self.character, self.extraAttributes)) 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: 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, #Methods to register and get the thing currently affecting the fit,
#so we can correctly map "Affected By" #so we can correctly map "Affected By"
def register(self, currModifier): def register(self, currModifier, origin=None):
self.__modifier = currModifier self.__modifier = currModifier
self.__origin = origin
if hasattr(currModifier, "itemModifiedAttributes"): if hasattr(currModifier, "itemModifiedAttributes"):
currModifier.itemModifiedAttributes.fit = self currModifier.itemModifiedAttributes.fit = self
if hasattr(currModifier, "chargeModifiedAttributes"): if hasattr(currModifier, "chargeModifiedAttributes"):
@@ -363,70 +383,16 @@ class Fit(object):
def getModifier(self): def getModifier(self):
return self.__modifier return self.__modifier
def calculateModifiedAttributes(self, targetFit=None, withBoosters=False, dirtyStorage=None): def getOrigin(self):
refreshBoosts = False return self.__origin
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)
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
else:
return
# If fit is calculated and we have nothing to do here, get out def __calculateGangBoosts(self, runTime):
if self.__calculated == True and forceProjected == False: logger.debug("Applying gang boosts in `%s` runtime for %s", runTime, 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)
if self.gangBoosts is not None:
contextMap = {Skill: "skill",
Ship: "ship",
Module: "module",
Implant: "implant"}
for name, info in self.gangBoosts.iteritems(): for name, info in self.gangBoosts.iteritems():
# Unpack all data required to run effect properly # Unpack all data required to run effect properly
effect, thing = info[1] effect, thing = info[1]
if effect.runTime == runTime: if effect.runTime == runTime:
context = ("gang", contextMap[type(thing)]) context = ("gang", thing.__class__.__name__.lower())
if isinstance(thing, Module): if isinstance(thing, Module):
if effect.isType("offline") or (effect.isType("passive") and thing.state >= State.ONLINE) or \ if effect.isType("offline") or (effect.isType("passive") and thing.state >= State.ONLINE) or \
(effect.isType("active") and thing.state >= State.ACTIVE): (effect.isType("active") and thing.state >= State.ACTIVE):
@@ -444,18 +410,114 @@ class Fit(object):
except: except:
pass pass
def calculateModifiedAttributes(self, targetFit=None, withBoosters=False, dirtyStorage=None):
timer = Timer('Fit: {}, {}'.format(self.ID, self.name), logger)
logger.debug("Starting fit calculation on: %s", repr(self))
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)
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:
logger.debug("Fleet is set, gathering gang boosts")
self.gangBoosts = self.fleet.recalculateLinear(withBoosters=withBoosters, dirtyStorage=dirtyStorage)
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
projected = False
else:
projected = True
# If fit is calculated and we have nothing to do here, get out
# 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")
return
# Mark fit as calculated
self.__calculated = True
for runTime in ("early", "normal", "late"):
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.projectedDrones, self.projectedModules)
# We calculate gang bonuses first so that projected fits get them
if self.gangBoosts is not None:
self.__calculateGangBoosts(runTime)
for item in c: 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: if item is not None:
self.register(item) self.register(item)
item.calculateModifiedAttributes(self, runTime, False) item.calculateModifiedAttributes(self, runTime, False)
if forceProjected is True: if projected is True:
targetFit.register(item) for _ in xrange(projectionInfo.amount):
targetFit.register(item, origin=self)
item.calculateModifiedAttributes(targetFit, runTime, True) item.calculateModifiedAttributes(targetFit, runTime, True)
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: for fit in self.projectedFits:
if fit.getProjectionInfo(self.ID).active:
fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage) 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): def fill(self):
""" """
Fill this fit's module slots with enough dummy slots so that all slots are used. Fill this fit's module slots with enough dummy slots so that all slots are used.
@@ -927,6 +989,19 @@ class Fit(object):
c.append(deepcopy(i, memo)) c.append(deepcopy(i, memo))
for fit in self.projectedFits: 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 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
)

View File

@@ -639,6 +639,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
copy.state = self.state copy.state = self.state
return copy 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): class Rack(Module):
''' '''
This is simply the Module class named something else to differentiate This is simply the Module class named something else to differentiate

View File

@@ -26,6 +26,17 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Ship(ItemAttrShortcut, HandledItem): 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): def __init__(self, item):
if item.category.name != "Ship": if item.category.name != "Ship":
@@ -34,7 +45,8 @@ class Ship(ItemAttrShortcut, HandledItem):
self.__item = item self.__item = item
self.__modeItems = self.__getModeItems() self.__modeItems = self.__getModeItems()
self.__itemModifiedAttributes = ModifiedAttributeDict() 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 self.commandBonus = 0

View File

@@ -17,5 +17,6 @@ __all__ = [
"changeAffectingSkills", "changeAffectingSkills",
"tacticalMode", "tacticalMode",
"targetResists", "targetResists",
"priceClear" "priceClear",
"amount",
] ]

View File

@@ -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

View File

@@ -29,67 +29,3 @@ class Cargo(ContextMenu):
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID))
Cargo.register() 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

View File

@@ -21,6 +21,8 @@
from gui import builtinViewColumns from gui import builtinViewColumns
from gui.viewColumn import ViewColumn from gui.viewColumn import ViewColumn
from gui import bitmapLoader from gui import bitmapLoader
import gui.mainFrame
import wx import wx
from eos.types import Drone, Cargo, Fit, Module, Slot, Rack from eos.types import Drone, Cargo, Fit, Module, Slot, Rack
import service import service
@@ -29,6 +31,7 @@ class BaseName(ViewColumn):
name = "Base Name" name = "Base Name"
def __init__(self, fittingView, params): def __init__(self, fittingView, params):
ViewColumn.__init__(self, fittingView) ViewColumn.__init__(self, fittingView)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.columnText = "Name" self.columnText = "Name"
self.shipImage = fittingView.imageList.GetImageIndex("ship_small", "icons") self.shipImage = fittingView.imageList.GetImageIndex("ship_small", "icons")
self.mask = wx.LIST_MASK_TEXT self.mask = wx.LIST_MASK_TEXT
@@ -39,7 +42,8 @@ class BaseName(ViewColumn):
elif isinstance(stuff, Cargo): elif isinstance(stuff, Cargo):
return "%dx %s" % (stuff.amount, stuff.item.name) return "%dx %s" % (stuff.amount, stuff.item.name)
elif isinstance(stuff, Fit): elif isinstance(stuff, Fit):
return "%s (%s)" % (stuff.name, stuff.ship.item.name) fitID = self.mainFrame.getActiveFit()
return "%dx %s (%s)" % (stuff.getProjectionInfo(fitID).amount, stuff.name, stuff.ship.item.name)
elif isinstance(stuff, Rack): elif isinstance(stuff, Rack):
if service.Fit.getInstance().serviceFittingOptions["rackLabels"]: if service.Fit.getInstance().serviceFittingOptions["rackLabels"]:
if stuff.slot == Slot.MODE: if stuff.slot == Slot.MODE:

View File

@@ -19,27 +19,21 @@
from gui.viewColumn import ViewColumn from gui.viewColumn import ViewColumn
from gui import bitmapLoader from gui import bitmapLoader
import gui.mainFrame
import wx import wx
from eos.types import Drone, Module, Rack from eos.types import Drone, Module, Rack, Fit
from eos.types import State as State_ from eos.types import State as State_
class State(ViewColumn): class State(ViewColumn):
name = "State" name = "State"
def __init__(self, fittingView, params): def __init__(self, fittingView, params):
ViewColumn.__init__(self, fittingView) ViewColumn.__init__(self, fittingView)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.resizable = False self.resizable = False
self.size = 16 self.size = 16
self.maxsize = self.size self.maxsize = self.size
self.mask = wx.LIST_MASK_IMAGE 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): def getText(self, mod):
return "" return ""
@@ -49,8 +43,14 @@ class State(ViewColumn):
return State_.getName(mod.state).title() return State_.getName(mod.state).title()
def getImageId(self, stuff): 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): 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): elif isinstance(stuff, Rack):
return -1 return -1
elif isinstance(stuff, Module): elif isinstance(stuff, Module):
@@ -58,11 +58,21 @@ class State(ViewColumn):
return -1 return -1
else: else:
return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(stuff.state).lower(), "icons") 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: else:
active = getattr(stuff, "active", None) active = getattr(stuff, "active", None)
if active is None: if active is None:
return -1 return -1
else: if active:
return self.checkedId if active else self.uncheckedId return generic_active
return generic_inactive
State.register() State.register()

View File

@@ -24,7 +24,7 @@ import bitmapLoader
import sys import sys
import wx.lib.mixins.listctrl as listmix import wx.lib.mixins.listctrl as listmix
import wx.html 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 from gui.utils.numberFormatter import formatAmount
import service import service
import config import config
@@ -549,15 +549,20 @@ class ItemEffects (wx.Panel):
class ItemAffectedBy (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): def __init__(self, parent, stuff, item):
wx.Panel.__init__ (self, parent) wx.Panel.__init__(self, parent)
self.stuff = stuff self.stuff = stuff
self.item = item self.item = item
self.toggleView = 1 self.activeFit = gui.mainFrame.MainFrame.getInstance().getActiveFit()
self.showRealNames = False
self.showAttrView = False
self.expand = -1 self.expand = -1
self.treeItems = []
mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer = wx.BoxSizer(wx.VERTICAL)
self.affectedBy = wx.TreeCtrl(self, style = wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.NO_BORDER) self.affectedBy = wx.TreeCtrl(self, style = wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.NO_BORDER)
@@ -571,7 +576,10 @@ class ItemAffectedBy (wx.Panel):
self.toggleExpandBtn = wx.ToggleButton( self, wx.ID_ANY, u"Expand All", wx.DefaultPosition, wx.DefaultSize, 0 ) 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) 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) bSizer.Add( self.toggleViewBtn, 0, wx.ALIGN_CENTER_VERTICAL)
if stuff is not None: if stuff is not None:
@@ -579,8 +587,9 @@ class ItemAffectedBy (wx.Panel):
bSizer.Add( self.refreshBtn, 0, wx.ALIGN_CENTER_VERTICAL) bSizer.Add( self.refreshBtn, 0, wx.ALIGN_CENTER_VERTICAL)
self.refreshBtn.Bind( wx.EVT_BUTTON, self.RefreshTree ) 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.toggleExpandBtn.Bind(wx.EVT_TOGGLEBUTTON,self.ToggleExpand)
self.toggleViewBtn.Bind(wx.EVT_TOGGLEBUTTON,self.ToggleViewMode)
mainSizer.Add( bSizer, 0, wx.ALIGN_RIGHT) mainSizer.Add( bSizer, 0, wx.ALIGN_RIGHT)
self.SetSizer(mainSizer) self.SetSizer(mainSizer)
@@ -607,18 +616,11 @@ class ItemAffectedBy (wx.Panel):
def ToggleViewTree(self): def ToggleViewTree(self):
self.Freeze() self.Freeze()
root = self.affectedBy.GetRootItem() for item in self.treeItems:
child,cookie = self.affectedBy.GetFirstChild(root)
while child.IsOk():
item,childcookie = self.affectedBy.GetFirstChild(child)
while item.IsOk():
change = self.affectedBy.GetPyData(item) change = self.affectedBy.GetPyData(item)
display = self.affectedBy.GetItemText(item) display = self.affectedBy.GetItemText(item)
self.affectedBy.SetItemText(item,change) self.affectedBy.SetItemText(item, change)
self.affectedBy.SetPyData(item,display) self.affectedBy.SetPyData(item, display)
item,childcookie = self.affectedBy.GetNextChild(child,childcookie)
child,cookie = self.affectedBy.GetNextChild(root,cookie)
self.Thaw() self.Thaw()
@@ -633,33 +635,213 @@ class ItemAffectedBy (wx.Panel):
event.Skip() event.Skip()
def ToggleViewMode(self, event): 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() self.ToggleViewTree()
event.Skip() event.Skip()
def PopulateTree(self): def PopulateTree(self):
# sheri was here
del self.treeItems[:]
root = self.affectedBy.AddRoot("WINPWNZ0R") root = self.affectedBy.AddRoot("WINPWNZ0R")
self.affectedBy.SetPyData(root, None) self.affectedBy.SetPyData(root, None)
self.imageList = wx.ImageList(16, 16) self.imageList = wx.ImageList(16, 16)
self.affectedBy.SetImageList(self.imageList) self.affectedBy.SetImageList(self.imageList)
cont = self.stuff.itemModifiedAttributes if self.item == self.stuff.item else self.stuff.chargeModifiedAttributes if self.showAttrView:
things = {} 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 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 continue
for fit, afflictors in cont.getAfflictions(attrName).iteritems():
for fit, afflictors in attributes.getAfflictions(attrName).iteritems():
for afflictor, modifier, amount, used in afflictors: for afflictor, modifier, amount, used in afflictors:
if not used or afflictor.item is None: if not used or afflictor.item is None:
continue continue
if afflictor.item.name not in things: if fit.ID != self.activeFit:
things[afflictor.item.name] = [type(afflictor), set(), []] # 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), 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, 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)
self.affectedBy.AppendItem(child, display, itemIcon)
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 afflictor.item 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) info[1].add(afflictor)
# If info[1] > 1, there are two separate modules working. # If info[1] > 1, there are two separate modules working.
# Check to make sure we only include the modifier once # Check to make sure we only include the modifier once
@@ -668,25 +850,47 @@ class ItemAffectedBy (wx.Panel):
continue continue
info[2].append((attrName, modifier, amount)) info[2].append((attrName, modifier, amount))
order = things.keys() # Make sure projected fits are on top
order.sort(key=lambda x: (self.ORDER.index(things[x][0]), x)) 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
items = container[thing]
order = items.keys()
order.sort(key=lambda x: (self.ORDER.index(items[x][0]), x))
for itemName in order: for itemName in order:
info = things[itemName] info = items[itemName]
afflictorType, afflictors, attrData = info afflictorType, afflictors, attrData, item, projected = info
counter = len(afflictors) counter = len(afflictors)
baseAfflictor = afflictors.pop()
if afflictorType == Ship: if afflictorType == Ship:
itemIcon = self.imageList.Add(bitmapLoader.getBitmap("ship_small", "icons")) itemIcon = self.imageList.Add(bitmapLoader.getBitmap("ship_small", "icons"))
elif baseAfflictor.item.icon: elif item.icon:
bitmap = bitmapLoader.getBitmap(baseAfflictor.item.icon.iconFile, "pack") bitmap = bitmapLoader.getBitmap(item.icon.iconFile, "pack")
itemIcon = self.imageList.Add(bitmap) if bitmap else -1 itemIcon = self.imageList.Add(bitmap) if bitmap else -1
else: else:
itemIcon = -1 itemIcon = -1
child = self.affectedBy.AppendItem(root, "%s" % itemName if counter == 1 else "%s x %d" % (itemName,counter), itemIcon) displayStr = itemName
if counter > 1:
displayStr += " x {}".format(counter)
if projected:
displayStr += " (projected)"
# this is the Module node, the attribute will be attached to this
child = self.affectedBy.AppendItem(parent, displayStr, itemIcon)
if counter > 0: if counter > 0:
attributes = [] attributes = []
@@ -716,15 +920,16 @@ class ItemAffectedBy (wx.Panel):
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: for attr in attrSorted:
attrName, displayName, attrModifier, attrAmount, penalized, attrIcon = attr 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) if self.showRealNames:
self.affectedBy.SetPyData(treeitem,"%s %s %.2f %s" % (attrName, attrModifier, attrAmount, penalized)) display = "%s %s %.2f %s" % (attrName, attrModifier, attrAmount, penalized)
saved = "%s %s %.2f %s" % ((displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized)
else: else:
treeitem = self.affectedBy.AppendItem(child, "%s %s %.2f %s" % (attrName, attrModifier, attrAmount, penalized), attrIcon) display = "%s %s %.2f %s" % ((displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized)
self.affectedBy.SetPyData(treeitem,"%s %s %.2f %s" % ((displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized)) saved = "%s %s %.2f %s" % (attrName, attrModifier, attrAmount, penalized)
self.ExpandCollapseTree()
treeitem = self.affectedBy.AppendItem(child, display, attrIcon)
self.affectedBy.SetPyData(treeitem, saved)
self.treeItems.append(treeitem)

View File

@@ -36,7 +36,7 @@ from service.fleet import Fleet
from service.settings import SettingsProvider from service.settings import SettingsProvider
from service.port import Port from service.port import Port
logger = logging.getLogger("pyfa.service.fit") logger = logging.getLogger(__name__)
class FitBackupThread(threading.Thread): class FitBackupThread(threading.Thread):
def __init__(self, path, callback): def __init__(self, path, callback):
@@ -175,10 +175,14 @@ class Fit(object):
fit = eos.db.getFit(fitID) fit = eos.db.getFit(fitID)
sFleet = Fleet.getInstance() sFleet = Fleet.getInstance()
sFleet.removeAssociatedFleetData(fit) sFleet.removeAssociatedFleetData(fit)
self.removeProjectedData(fitID)
eos.db.remove(fit) 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): def copyFit(self, fitID):
fit = eos.db.getFit(fitID) fit = eos.db.getFit(fitID)
newFit = copy.deepcopy(fit) newFit = copy.deepcopy(fit)
@@ -193,14 +197,6 @@ class Fit(object):
fit.clear() fit.clear()
return fit 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): def toggleFactorReload(self, fitID):
if fitID is None: if fitID is None:
return None return None
@@ -236,6 +232,7 @@ class Fit(object):
return None return None
fit = eos.db.getFit(fitID) fit = eos.db.getFit(fitID)
inited = getattr(fit, "inited", None) inited = getattr(fit, "inited", None)
if inited is None or inited is False: if inited is None or inited is False:
sFleet = Fleet.getInstance() sFleet = Fleet.getInstance()
f = sFleet.getLinearFleet(fit) f = sFleet.getLinearFleet(fit)
@@ -246,6 +243,7 @@ class Fit(object):
fit.fleet = f fit.fleet = f
if not projected: if not projected:
print "Not projected, getting projected fits"
for fitP in fit.projectedFits: for fitP in fit.projectedFits:
self.getFit(fitP.ID, projected = True) self.getFit(fitP.ID, projected = True)
self.recalc(fit, withBoosters=True) self.recalc(fit, withBoosters=True)
@@ -322,9 +320,14 @@ class Fit(object):
eager=("attributes", "group.category")) eager=("attributes", "group.category"))
if isinstance(thing, eos.types.Fit): if isinstance(thing, eos.types.Fit):
if thing.ID == fitID: if thing in fit.projectedFits:
return 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": elif thing.category.name == "Drone":
drone = None drone = None
for d in fit.projectedDrones.find(thing): for d in fit.projectedDrones.find(thing):
@@ -363,6 +366,22 @@ class Fit(object):
thing.state = self.__getProposedState(thing, click) thing.state = self.__getProposedState(thing, click)
if not thing.canHaveState(thing.state, fit): if not thing.canHaveState(thing.state, fit):
thing.state = State.OFFLINE 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() eos.db.commit()
self.recalc(fit) self.recalc(fit)
@@ -374,7 +393,8 @@ class Fit(object):
elif isinstance(thing, eos.types.Module): elif isinstance(thing, eos.types.Module):
fit.projectedModules.remove(thing) fit.projectedModules.remove(thing)
else: else:
fit.projectedFits.remove(thing) del fit.__projectedFits[thing.ID]
#fit.projectedFits.remove(thing)
eos.db.commit() eos.db.commit()
self.recalc(fit) self.recalc(fit)
@@ -921,7 +941,8 @@ class Fit(object):
eos.db.commit() eos.db.commit()
self.recalc(fit) 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"]: if fit.factorReload is not self.serviceFittingOptions["useGlobalForceReload"]:
fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"] fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"]
fit.clear() fit.clear()

View File

@@ -1,30 +1,36 @@
import time import time
class Timer(object): class Timer():
""" def __init__(self, name='', logger=None):
Generic timing class for simple profiling. 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: @property
# code to be timed def last(self):
time.sleep(5) return (time.time() - self.__last)*1000
Output: def checkpoint(self, name=''):
elapsed time: 5000.000 ms text = 'Timer - {timer} - {checkpoint} - {last:.2f}ms ({elapsed:.2f}ms elapsed)'.format(
timer=self.name,
Can also access time with t.secs checkpoint=name,
""" last=self.last,
def __init__(self, verbose=False): elapsed=self.elapsed
self.verbose = verbose ).strip()
self.__last = time.time()
if self.logger:
self.logger.debug(text)
else:
print text
def __enter__(self): def __enter__(self):
self.start = time.time()
return self return self
def __exit__(self, *args): def __exit__(self, type, value, traceback):
self.end = time.time() self.checkpoint('finished')
self.secs = self.end - self.start pass
self.msecs = self.secs * 1000 # millisecs
if self.verbose:
print 'elapsed time: %f ms' % self.msecs