Merge branch 'origin_master' into effect_rollup

This commit is contained in:
Ryan Holmes
2019-03-21 19:57:45 -04:00
101 changed files with 1498 additions and 853 deletions

View File

@@ -129,7 +129,8 @@ def Saveddata():
from eos.saveddata.ship import Ship from eos.saveddata.ship import Ship
from eos.saveddata.fit import Fit from eos.saveddata.fit import Fit
from eos.saveddata.character import Character from eos.saveddata.character import Character
from eos.saveddata.module import Module, State from eos.saveddata.module import Module
from eos.const import FittingModuleState
from eos.saveddata.citadel import Citadel from eos.saveddata.citadel import Citadel
from eos.saveddata.booster import Booster from eos.saveddata.booster import Booster
@@ -139,7 +140,7 @@ def Saveddata():
'Fit' : Fit, 'Fit' : Fit,
'Character': Character, 'Character': Character,
'Module' : Module, 'Module' : Module,
'State' : State, 'State' : FittingModuleState,
'Booster' : Booster, 'Booster' : Booster,
} }
return helper return helper

View File

@@ -6,7 +6,7 @@ import wx
from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \ from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \
StreamHandler, TimedRotatingFileHandler, WARNING StreamHandler, TimedRotatingFileHandler, WARNING
import hashlib import hashlib
from eos.const import Slot from eos.const import FittingSlot
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
@@ -50,11 +50,11 @@ LOGLEVEL_MAP = {
} }
slotColourMap = { slotColourMap = {
Slot.LOW: wx.Colour(250, 235, 204), # yellow = low slots FittingSlot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
Slot.MED: wx.Colour(188, 215, 241), # blue = mid slots FittingSlot.MED: wx.Colour(188, 215, 241), # blue = mid slots
Slot.HIGH: wx.Colour(235, 204, 209), # red = high slots FittingSlot.HIGH: wx.Colour(235, 204, 209), # red = high slots
Slot.RIG: '', FittingSlot.RIG: '',
Slot.SUBSYSTEM: '' FittingSlot.SUBSYSTEM: ''
} }
def getClientSecret(): def getClientSecret():
@@ -95,7 +95,7 @@ def defPaths(customSavePath=None):
global pyfaPath global pyfaPath
global savePath global savePath
global saveDB global saveDB
global gameDB global gameDB
global saveInRoot global saveInRoot
global logPath global logPath
global cipher global cipher

View File

@@ -21,13 +21,14 @@ if istravis is True or hasattr(sys, '_called_from_test'):
# Running in Travis. Run saveddata database in memory. # Running in Travis. Run saveddata database in memory.
saveddata_connectionstring = 'sqlite:///:memory:' saveddata_connectionstring = 'sqlite:///:memory:'
else: else:
saveddata_connectionstring = 'sqlite:///' + realpath(join(dirname(abspath(__file__)), "..", "saveddata", "saveddata-py3-db.db")) saveddata_connectionstring = 'sqlite:///' + realpath(join(dirname(abspath(__file__)), "..", "saveddata", "saveddata.db"))
pyfalog.debug("Saveddata connection string: {0}", saveddata_connectionstring) pyfalog.debug("Saveddata connection string: {0}", saveddata_connectionstring)
settings = { settings = {
"useStaticAdaptiveArmorHardener": False, "useStaticAdaptiveArmorHardener": False,
"strictSkillLevels": True, "strictSkillLevels": True,
"globalDefaultSpoolupPercentage": 1.0
} }
# Autodetect path, only change if the autodetection bugs out. # Autodetect path, only change if the autodetection bugs out.

View File

@@ -1,8 +1,30 @@
from eos.enum import Enum # =============================================================================
# Copyright (C) 2019 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
from enum import IntEnum,unique
@unique
class Slot(Enum): class FittingSlot(IntEnum):
"""
Contains slots for ship fittings
"""
# These are self-explanatory # These are self-explanatory
LOW = 1 LOW = 1
MED = 2 MED = 2
@@ -24,3 +46,50 @@ class Slot(Enum):
FS_LIGHT = 13 FS_LIGHT = 13
FS_SUPPORT = 14 FS_SUPPORT = 14
FS_HEAVY = 15 FS_HEAVY = 15
@unique
class ImplantLocation(IntEnum):
"""
Contains location of the implant
"""
FIT = 0
CHARACTER = 1
@unique
class CalcType(IntEnum):
"""
Contains location of the calculation
"""
LOCAL = 0
PROJECTED = 1
COMMAND = 2
@unique
class FittingModuleState(IntEnum):
"""
Contains the state of a fitting module
"""
OFFLINE = -1
ONLINE = 0
ACTIVE = 1
OVERHEATED = 2
@unique
class FittingHardpoint(IntEnum):
"""
Contains the types of a fitting hardpoint
"""
NONE = 0
MISSILE = 1
TURRET = 2
@unique
class SpoolType(IntEnum):
SCALE = 0 # [0..1]
TIME = 1 # Expressed via time in seconds since spool up started
CYCLES = 2 # Expressed in amount of cycles since spool up started

View File

@@ -41,8 +41,7 @@ items_table = Table("invtypes", gamedata_meta,
Column("iconID", Integer), Column("iconID", Integer),
Column("graphicID", Integer), Column("graphicID", Integer),
Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True), Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True),
Column("replaceSame", String), Column("replacements", String))
Column("replaceBetter", String))
from .metaGroup import metatypes_table # noqa from .metaGroup import metatypes_table # noqa
from .traits import traits_table # noqa from .traits import traits_table # noqa

View File

@@ -32,6 +32,4 @@ prices_table = Table("prices", saveddata_meta,
Column("status", Integer, nullable=False)) Column("status", Integer, nullable=False))
mapper(Price, prices_table, properties={ mapper(Price, prices_table)
"_Price__price": prices_table.c.price,
})

View File

@@ -21,6 +21,7 @@ import sys
from sqlalchemy.sql import and_ from sqlalchemy.sql import and_
from sqlalchemy import desc, select from sqlalchemy import desc, select
from sqlalchemy import func
from eos.db import saveddata_session, sd_lock from eos.db import saveddata_session, sd_lock
from eos.db.saveddata.fit import projectedFits_table from eos.db.saveddata.fit import projectedFits_table
@@ -283,6 +284,12 @@ def countAllFits():
return count return count
def countFitGroupedByShip():
with sd_lock:
count = eos.db.saveddata_session.query(Fit.shipID, func.count(Fit.shipID)).group_by(Fit.shipID).all()
return count
def countFitsWithShip(lookfor, ownerID=None, where=None, eager=None): def countFitsWithShip(lookfor, ownerID=None, where=None, eager=None):
""" """
Get all the fits using a certain ship. Get all the fits using a certain ship.

View File

@@ -7,7 +7,7 @@
# #
# Used by: # Used by:
# Modules from group: Warp Disrupt Field Generator (7 of 7) # Modules from group: Warp Disrupt Field Generator (7 of 7)
from eos.saveddata.module import State from eos.const import FittingModuleState
type = "projected", "active" type = "projected", "active"
runTime = "early" runTime = "early"
@@ -19,10 +19,10 @@ def handler(fit, module, context):
fit.ship.increaseItemAttr("warpScrambleStatus", module.getModifiedItemAttr("warpScrambleStrength")) fit.ship.increaseItemAttr("warpScrambleStatus", module.getModifiedItemAttr("warpScrambleStrength"))
if module.charge is not None and module.charge.ID == 45010: if module.charge is not None and module.charge.ID == 45010:
for mod in fit.modules: for mod in fit.modules:
if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > State.ONLINE: if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > FittingModuleState.ONLINE:
mod.state = State.ONLINE mod.state = FittingModuleState.ONLINE
if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE: if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > FittingModuleState.ONLINE:
mod.state = State.ONLINE mod.state = FittingModuleState.ONLINE
else: else:
if module.charge is None: if module.charge is None:
fit.ship.boostItemAttr("mass", module.getModifiedItemAttr("massBonusPercentage")) fit.ship.boostItemAttr("mass", module.getModifiedItemAttr("massBonusPercentage"))

View File

@@ -2,7 +2,7 @@
# #
# Used by: # Used by:
# Modules named like: Warp Scrambler (27 of 27) # Modules named like: Warp Scrambler (27 of 27)
from eos.saveddata.module import State from eos.const import FittingModuleState
runTime = "early" runTime = "early"
type = "projected", "active" type = "projected", "active"
@@ -16,10 +16,10 @@ def handler(fit, module, context):
# this is such a dirty hack # this is such a dirty hack
for mod in fit.modules: for mod in fit.modules:
if not mod.isEmpty and mod.state > State.ONLINE and ( if not mod.isEmpty and mod.state > FittingModuleState.ONLINE and (
mod.item.requiresSkill("Micro Jump Drive Operation") or mod.item.requiresSkill("Micro Jump Drive Operation") or
mod.item.requiresSkill("High Speed Maneuvering") mod.item.requiresSkill("High Speed Maneuvering")
): ):
mod.state = State.ONLINE mod.state = FittingModuleState.ONLINE
if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE: if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > FittingModuleState.ONLINE:
mod.state = State.ONLINE mod.state = FittingModuleState.ONLINE

View File

@@ -2,15 +2,15 @@
# #
# Used by: # Used by:
# Modules from group: Energy Neutralizer (54 of 54) # Modules from group: Energy Neutralizer (54 of 54)
from eos.saveddata.module import State from eos.const import FittingModuleState
from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.modifiedAttributeDict import ModifiedAttributeDict
type = "active", "projected" type = "active", "projected"
def handler(fit, src, context, **kwargs): def handler(fit, src, context, **kwargs):
if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or if "projected" in context and ((hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or
hasattr(src, "amountActive")): hasattr(src, "amountActive")):
amount = src.getModifiedItemAttr("energyNeutralizerAmount") amount = src.getModifiedItemAttr("energyNeutralizerAmount")
if 'effect' in kwargs: if 'effect' in kwargs:

View File

@@ -2,7 +2,7 @@
# #
# Used by: # Used by:
# Structure Modules from group: Structure Energy Neutralizer (5 of 5) # Structure Modules from group: Structure Energy Neutralizer (5 of 5)
from eos.saveddata.module import State from eos.const import FittingModuleState
from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.modifiedAttributeDict import ModifiedAttributeDict
type = "active", "projected" type = "active", "projected"
@@ -11,7 +11,7 @@ type = "active", "projected"
def handler(fit, src, context, **kwargs): def handler(fit, src, context, **kwargs):
amount = 0 amount = 0
if "projected" in context: if "projected" in context:
if (hasattr(src, "state") and src.state >= State.ACTIVE) or hasattr(src, "amountActive"): if (hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or hasattr(src, "amountActive"):
amount = src.getModifiedItemAttr("energyNeutralizerAmount") amount = src.getModifiedItemAttr("energyNeutralizerAmount")
if 'effect' in kwargs: if 'effect' in kwargs:

View File

@@ -2,7 +2,7 @@
# #
# Used by: # Used by:
# Structure Modules from group: Structure Warp Scrambler (2 of 2) # Structure Modules from group: Structure Warp Scrambler (2 of 2)
from eos.saveddata.module import State from eos.const import FittingModuleState
# Not used by any item # Not used by any item
runTime = "early" runTime = "early"
@@ -14,7 +14,7 @@ def handler(fit, module, context):
fit.ship.increaseItemAttr("warpScrambleStatus", module.getModifiedItemAttr("warpScrambleStrength")) fit.ship.increaseItemAttr("warpScrambleStatus", module.getModifiedItemAttr("warpScrambleStrength"))
if module.charge is not None and module.charge.ID == 47336: if module.charge is not None and module.charge.ID == 47336:
for mod in fit.modules: for mod in fit.modules:
if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > State.ONLINE: if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > FittingModuleState.ONLINE:
mod.state = State.ONLINE mod.state = FittingModuleState.ONLINE
if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE: if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > FittingModuleState.ONLINE:
mod.state = State.ONLINE mod.state = FittingModuleState.ONLINE

View File

@@ -3,15 +3,15 @@
# Used by: # Used by:
# Module: Energy Neutralization Burst Projector # Module: Energy Neutralization Burst Projector
# Structure Module: Standup Energy Neutralization Burst Projector # Structure Module: Standup Energy Neutralization Burst Projector
from eos.saveddata.module import State from eos.const import FittingModuleState
from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.modifiedAttributeDict import ModifiedAttributeDict
type = "active", "projected" type = "active", "projected"
def handler(fit, src, context, **kwargs): def handler(fit, src, context, **kwargs):
if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or if "projected" in context and ((hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or
hasattr(src, "amountActive")): hasattr(src, "amountActive")):
amount = src.getModifiedItemAttr("energyNeutralizerAmount") amount = src.getModifiedItemAttr("energyNeutralizerAmount")
if 'effect' in kwargs: if 'effect' in kwargs:

View File

@@ -2,15 +2,15 @@
# #
# Used by: # Used by:
# Drones from group: Energy Neutralizer Drone (3 of 3) # Drones from group: Energy Neutralizer Drone (3 of 3)
from eos.saveddata.module import State from eos.const import FittingModuleState
from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.modifiedAttributeDict import ModifiedAttributeDict
type = "active", "projected" type = "active", "projected"
def handler(fit, src, context, **kwargs): def handler(fit, src, context, **kwargs):
if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or if "projected" in context and ((hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or
hasattr(src, "amountActive")): hasattr(src, "amountActive")):
amount = src.getModifiedItemAttr("energyNeutralizerAmount") amount = src.getModifiedItemAttr("energyNeutralizerAmount")
time = src.getModifiedItemAttr("energyNeutralizerDuration") time = src.getModifiedItemAttr("energyNeutralizerDuration")

View File

@@ -4,6 +4,7 @@
# Modules from group: Mutadaptive Remote Armor Repairer (5 of 5) # Modules from group: Mutadaptive Remote Armor Repairer (5 of 5)
import eos.config
from eos.utils.spoolSupport import SpoolType, SpoolOptions, calculateSpoolup, resolveSpoolOptions from eos.utils.spoolSupport import SpoolType, SpoolOptions, calculateSpoolup, resolveSpoolOptions
@@ -17,8 +18,7 @@ def handler(fit, container, context, **kwargs):
cycleTime = container.getModifiedItemAttr("duration") / 1000.0 cycleTime = container.getModifiedItemAttr("duration") / 1000.0
repSpoolMax = container.getModifiedItemAttr("repairMultiplierBonusMax") repSpoolMax = container.getModifiedItemAttr("repairMultiplierBonusMax")
repSpoolPerCycle = container.getModifiedItemAttr("repairMultiplierBonusPerCycle") repSpoolPerCycle = container.getModifiedItemAttr("repairMultiplierBonusPerCycle")
# TODO: fetch spoolup option defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
defaultSpoolValue = 1
spoolType, spoolAmount = resolveSpoolOptions(SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False), container) spoolType, spoolAmount = resolveSpoolOptions(SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False), container)
rps = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, spoolType, spoolAmount)[0]) / cycleTime rps = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, spoolType, spoolAmount)[0]) / cycleTime
rpsPreSpool = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, SpoolType.SCALE, 0)[0]) / cycleTime rpsPreSpool = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, SpoolType.SCALE, 0)[0]) / cycleTime

View File

@@ -0,0 +1,17 @@
runTime = "early"
type = "passive"
def handler(fit, module, context):
secModifier = module.getModifiedItemAttr("securityModifier")
module.multiplyItemAttr("structureRigDoomsdayDamageLossTargetBonus", secModifier)
module.multiplyItemAttr("structureRigScanResBonus", secModifier)
module.multiplyItemAttr("structureRigPDRangeBonus", secModifier)
module.multiplyItemAttr("structureRigPDCapUseBonus", secModifier)
module.multiplyItemAttr("structureRigMissileExploVeloBonus", secModifier)
module.multiplyItemAttr("structureRigMissileVelocityBonus", secModifier)
module.multiplyItemAttr("structureRigEwarOptimalBonus", secModifier)
module.multiplyItemAttr("structureRigEwarFalloffBonus", secModifier)
module.multiplyItemAttr("structureRigEwarCapUseBonus", secModifier)
module.multiplyItemAttr("structureRigMissileExplosionRadiusBonus", secModifier)
module.multiplyItemAttr("structureRigMaxTargetRangeBonus", secModifier)

View File

@@ -1,23 +0,0 @@
class Enum(object):
def __init__(self):
pass
@classmethod
def getTypes(cls):
for stuff in cls.__dict__:
if stuff.upper() == stuff:
yield stuff
@classmethod
def getName(cls, v):
map = getattr(cls, "_map", None)
if map is None:
map = cls._map = {}
for type in cls.getTypes():
map[cls.getValue(type)] = type
return map.get(v)
@classmethod
def getValue(cls, type):
return cls.__dict__[type]

View File

@@ -20,7 +20,7 @@
from math import log, sin, radians, exp from math import log, sin, radians, exp
from eos.graph import Graph from eos.graph import Graph
from eos.saveddata.module import State, Hardpoint from eos.const import FittingModuleState, FittingHardpoint
from logbook import Logger from logbook import Logger
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -46,7 +46,7 @@ class FitDpsGraph(Graph):
abssort = lambda _val: -abs(_val - 1) abssort = lambda _val: -abs(_val - 1)
for mod in fit.modules: for mod in fit.modules:
if not mod.isEmpty and mod.state >= State.ACTIVE: if not mod.isEmpty and mod.state >= FittingModuleState.ACTIVE:
if "remoteTargetPaintFalloff" in mod.item.effects or "structureModuleEffectTargetPainter" in mod.item.effects: if "remoteTargetPaintFalloff" in mod.item.effects or "structureModuleEffectTargetPainter" in mod.item.effects:
ew['signatureRadius'].append( ew['signatureRadius'].append(
1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) * self.calculateModuleMultiplier( 1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) * self.calculateModuleMultiplier(
@@ -76,12 +76,12 @@ class FitDpsGraph(Graph):
for mod in fit.modules: for mod in fit.modules:
dps = mod.getDps(targetResists=fit.targetResists).total dps = mod.getDps(targetResists=fit.targetResists).total
if mod.hardpoint == Hardpoint.TURRET: if mod.hardpoint == FittingHardpoint.TURRET:
if mod.state >= State.ACTIVE: if mod.state >= FittingModuleState.ACTIVE:
total += dps * self.calculateTurretMultiplier(mod, data) total += dps * self.calculateTurretMultiplier(mod, data)
elif mod.hardpoint == Hardpoint.MISSILE: elif mod.hardpoint == FittingHardpoint.MISSILE:
if mod.state >= State.ACTIVE and mod.maxRange is not None and mod.maxRange >= distance: if mod.state >= FittingModuleState.ACTIVE and mod.maxRange is not None and mod.maxRange >= distance:
total += dps * self.calculateMissileMultiplier(mod, data) total += dps * self.calculateMissileMultiplier(mod, data)
if distance <= fit.extraAttributes["droneControlRange"]: if distance <= fit.extraAttributes["droneControlRange"]:

View File

@@ -30,6 +30,7 @@ pyfalog = Logger(__name__)
class Booster(HandledItem, ItemAttrShortcut): class Booster(HandledItem, ItemAttrShortcut):
def __init__(self, item): def __init__(self, item):
self.__item = item self.__item = item
@@ -147,3 +148,17 @@ class Booster(HandledItem, ItemAttrShortcut):
copyEffect.active = sideEffect.active copyEffect.active = sideEffect.active
return copy return copy
def rebase(self, item):
active = self.active
sideEffectStates = {se.effectID: se.active for se in self.sideEffects}
Booster.__init__(self, item)
self.active = active
for sideEffect in self.sideEffects:
if sideEffect.effectID in sideEffectStates:
sideEffect.active = sideEffectStates[sideEffect.effectID]
def __repr__(self):
return "Booster(ID={}, name={}) at {}".format(
self.item.ID, self.item.name, hex(id(self))
)

View File

@@ -89,3 +89,13 @@ class Cargo(HandledItem, ItemAttrShortcut):
copy = Cargo(self.item) copy = Cargo(self.item)
copy.amount = self.amount copy.amount = self.amount
return copy return copy
def rebase(self, item):
amount = self.amount
Cargo.__init__(self, item)
self.amount = amount
def __repr__(self):
return "Cargo(ID={}, name={}) at {}".format(
self.item.ID, self.item.name, hex(id(self))
)

View File

@@ -296,6 +296,13 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
copy.amountActive = self.amountActive copy.amountActive = self.amountActive
return copy return copy
def rebase(self, item):
amount = self.amount
amountActive = self.amountActive
Drone.__init__(self, item)
self.amount = amount
self.amountActive = amountActive
def fits(self, fit): def fits(self, fit):
fitDroneGroupLimits = set() fitDroneGroupLimits = set()
for i in range(1, 3): for i in range(1, 3):

View File

@@ -25,8 +25,8 @@ import eos.db
from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.effectHandlerHelpers import HandledItem, HandledCharge
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.saveddata.fighterAbility import FighterAbility from eos.saveddata.fighterAbility import FighterAbility
from eos.saveddata.module import Slot
from eos.utils.stats import DmgTypes from eos.utils.stats import DmgTypes
from eos.const import FittingSlot
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -116,12 +116,12 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def __calculateSlot(self, item): def __calculateSlot(self, item):
types = { types = {
"Light" : Slot.F_LIGHT, "Light" : FittingSlot.F_LIGHT,
"Support": Slot.F_SUPPORT, "Support": FittingSlot.F_SUPPORT,
"Heavy" : Slot.F_HEAVY, "Heavy" : FittingSlot.F_HEAVY,
"StandupLight": Slot.FS_LIGHT, "StandupLight": FittingSlot.FS_LIGHT,
"StandupSupport": Slot.FS_SUPPORT, "StandupSupport": FittingSlot.FS_SUPPORT,
"StandupHeavy": Slot.FS_HEAVY "StandupHeavy": FittingSlot.FS_HEAVY
} }
for t, slot in types.items(): for t, slot in types.items():
@@ -355,6 +355,17 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
copyAbility.active = ability.active copyAbility.active = ability.active
return copy return copy
def rebase(self, item):
amount = self.amount
active = self.active
abilityEffectStates = {a.effectID: a.active for a in self.abilities}
Fighter.__init__(self, item)
self.amount = amount
self.active = active
for ability in self.abilities:
if ability.effectID in abilityEffectStates:
ability.active = abilityEffectStates[ability.effectID]
def fits(self, fit): def fits(self, fit):
# If ships doesn't support this type of fighter, don't add it # If ships doesn't support this type of fighter, don't add it
if fit.getNumSlots(self.slot) == 0: if fit.getNumSlots(self.slot) == 0:

View File

@@ -28,28 +28,19 @@ from sqlalchemy.orm import validates, reconstructor
import eos.db import eos.db
from eos import capSim from eos import capSim
from eos.effectHandlerHelpers import HandledModuleList, HandledDroneCargoList, HandledImplantBoosterList, HandledProjectedDroneList, HandledProjectedModList from eos.effectHandlerHelpers import HandledModuleList, HandledDroneCargoList, HandledImplantBoosterList, HandledProjectedDroneList, HandledProjectedModList
from eos.enum import Enum from eos.const import ImplantLocation, CalcType, FittingSlot
from eos.saveddata.ship import Ship from eos.saveddata.ship import Ship
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.character import Character from eos.saveddata.character import Character
from eos.saveddata.citadel import Citadel from eos.saveddata.citadel import Citadel
from eos.saveddata.module import Module, State, Slot, Hardpoint from eos.const import FittingModuleState, FittingHardpoint
from eos.saveddata.module import Module
from eos.utils.stats import DmgTypes from eos.utils.stats import DmgTypes
from logbook import Logger from logbook import Logger
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
class ImplantLocation(Enum):
FIT = 0
CHARACTER = 1
class CalcType(Enum):
LOCAL = 0
PROJECTED = 1
COMMAND = 2
class Fit(object): class Fit(object):
"""Represents a fitting, with modules, ship, implants, etc.""" """Represents a fitting, with modules, ship, implants, etc."""
@@ -393,6 +384,32 @@ class Fit(object):
else: else:
return val return val
def canFit(self, item):
# Whereas Module.fits() deals with current state of the fit in order to determine if somethign fits (for example maxGroupFitted which can be modified by effects),
# this function should be used against Items to see if the item is even allowed on the fit with rules that don't change
fitsOnType = set()
fitsOnGroup = set()
shipType = item.attributes.get("fitsToShipType", None)
if shipType is not None:
fitsOnType.add(shipType.value)
fitsOnType.update([item.attributes[attr].value for attr in item.attributes if attr.startswith("canFitShipType")])
fitsOnGroup.update([item.attributes[attr].value for attr in item.attributes if attr.startswith("canFitShipGroup")])
if (len(fitsOnGroup) > 0 or len(fitsOnType) > 0) \
and self.ship.item.group.ID not in fitsOnGroup \
and self.ship.item.ID not in fitsOnType:
return False
# Citadel modules are now under a new category, so we can check this to ensure only structure modules can fit on a citadel
if isinstance(self.ship, Citadel) and item.category.name != "Structure Module" or \
not isinstance(self.ship, Citadel) and item.category.name == "Structure Module":
return False
return True
def clear(self, projected=False, command=False): def clear(self, projected=False, command=False):
self.__effectiveTank = None self.__effectiveTank = None
self.__weaponDpsMap = {} self.__weaponDpsMap = {}
@@ -741,7 +758,7 @@ class Fit(object):
The type of calculation our current iteration is in. This helps us determine the interactions between The type of calculation our current iteration is in. This helps us determine the interactions between
fits that rely on others for proper calculations fits that rely on others for proper calculations
""" """
pyfalog.info("Starting fit calculation on: {0}, calc: {1}", repr(self), CalcType.getName(type)) pyfalog.info("Starting fit calculation on: {0}, calc: {1}", repr(self), CalcType(type).name)
# If we are projecting this fit onto another one, collect the projection info for later use # If we are projecting this fit onto another one, collect the projection info for later use
@@ -901,7 +918,7 @@ class Fit(object):
if self.ship is None: if self.ship is None:
return return
for slotType in (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE): for slotType in (FittingSlot.LOW.value, FittingSlot.MED.value, FittingSlot.HIGH.value, FittingSlot.RIG.value, FittingSlot.SUBSYSTEM.value, FittingSlot.SERVICE.value):
amount = self.getSlotsFree(slotType, True) amount = self.getSlotsFree(slotType, True)
if amount > 0: if amount > 0:
for _ in range(int(amount)): for _ in range(int(amount)):
@@ -948,7 +965,7 @@ class Fit(object):
def getItemAttrOnlineSum(dict, attr): def getItemAttrOnlineSum(dict, attr):
amount = 0 amount = 0
for mod in dict: for mod in dict:
add = mod.getModifiedItemAttr(attr) if mod.state >= State.ONLINE else None add = mod.getModifiedItemAttr(attr) if mod.state >= FittingModuleState.ONLINE else None
if add is not None: if add is not None:
amount += add amount += add
@@ -967,29 +984,29 @@ class Fit(object):
for mod in chain(self.modules, self.fighters): for mod in chain(self.modules, self.fighters):
if mod.slot is type and (not getattr(mod, "isEmpty", False) or countDummies): if mod.slot is type and (not getattr(mod, "isEmpty", False) or countDummies):
if type in (Slot.F_HEAVY, Slot.F_SUPPORT, Slot.F_LIGHT, Slot.FS_HEAVY, Slot.FS_LIGHT, Slot.FS_SUPPORT) and not mod.active: if type in (FittingSlot.F_HEAVY, FittingSlot.F_SUPPORT, FittingSlot.F_LIGHT, FittingSlot.FS_HEAVY, FittingSlot.FS_LIGHT, FittingSlot.FS_SUPPORT) and not mod.active:
continue continue
amount += 1 amount += 1
return amount return amount
slots = { slots = {
Slot.LOW : "lowSlots", FittingSlot.LOW : "lowSlots",
Slot.MED : "medSlots", FittingSlot.MED : "medSlots",
Slot.HIGH : "hiSlots", FittingSlot.HIGH : "hiSlots",
Slot.RIG : "rigSlots", FittingSlot.RIG : "rigSlots",
Slot.SUBSYSTEM: "maxSubSystems", FittingSlot.SUBSYSTEM: "maxSubSystems",
Slot.SERVICE : "serviceSlots", FittingSlot.SERVICE : "serviceSlots",
Slot.F_LIGHT : "fighterLightSlots", FittingSlot.F_LIGHT : "fighterLightSlots",
Slot.F_SUPPORT: "fighterSupportSlots", FittingSlot.F_SUPPORT: "fighterSupportSlots",
Slot.F_HEAVY : "fighterHeavySlots", FittingSlot.F_HEAVY : "fighterHeavySlots",
Slot.FS_LIGHT: "fighterStandupLightSlots", FittingSlot.FS_LIGHT: "fighterStandupLightSlots",
Slot.FS_SUPPORT: "fighterStandupSupportSlots", FittingSlot.FS_SUPPORT: "fighterStandupSupportSlots",
Slot.FS_HEAVY: "fighterStandupHeavySlots", FittingSlot.FS_HEAVY: "fighterStandupHeavySlots",
} }
def getSlotsFree(self, type, countDummies=False): def getSlotsFree(self, type, countDummies=False):
if type in (Slot.MODE, Slot.SYSTEM): if type in (FittingSlot.MODE, FittingSlot.SYSTEM):
# These slots don't really exist, return default 0 # These slots don't really exist, return default 0
return 0 return 0
@@ -1001,12 +1018,12 @@ class Fit(object):
return self.ship.getModifiedItemAttr(self.slots[type]) or 0 return self.ship.getModifiedItemAttr(self.slots[type]) or 0
def getHardpointsFree(self, type): def getHardpointsFree(self, type):
if type == Hardpoint.NONE: if type == FittingHardpoint.NONE:
return 1 return 1
elif type == Hardpoint.TURRET: elif type == FittingHardpoint.TURRET:
return self.ship.getModifiedItemAttr('turretSlotsLeft') - self.getHardpointsUsed(Hardpoint.TURRET) return self.ship.getModifiedItemAttr('turretSlotsLeft') - self.getHardpointsUsed(FittingHardpoint.TURRET)
elif type == Hardpoint.MISSILE: elif type == FittingHardpoint.MISSILE:
return self.ship.getModifiedItemAttr('launcherSlotsLeft') - self.getHardpointsUsed(Hardpoint.MISSILE) return self.ship.getModifiedItemAttr('launcherSlotsLeft') - self.getHardpointsUsed(FittingHardpoint.MISSILE)
else: else:
raise ValueError("%d is not a valid value for Hardpoint Enum", type) raise ValueError("%d is not a valid value for Hardpoint Enum", type)
@@ -1168,7 +1185,7 @@ class Fit(object):
capUsed = 0 capUsed = 0
capAdded = 0 capAdded = 0
for mod in self.modules: for mod in self.modules:
if mod.state >= State.ACTIVE: if mod.state >= FittingModuleState.ACTIVE:
if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0: if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0:
cycleTime = mod.rawCycleTime or 0 cycleTime = mod.rawCycleTime or 0
reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0 reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0
@@ -1182,7 +1199,7 @@ class Fit(object):
capAdded -= capNeed capAdded -= capNeed
# If this is a turret, don't stagger activations # If this is a turret, don't stagger activations
disableStagger = mod.hardpoint == Hardpoint.TURRET disableStagger = mod.hardpoint == FittingHardpoint.TURRET
drains.append((int(fullCycleTime), mod.getModifiedItemAttr("capacitorNeed") or 0, drains.append((int(fullCycleTime), mod.getModifiedItemAttr("capacitorNeed") or 0,
mod.numShots or 0, disableStagger, reloadTime)) mod.numShots or 0, disableStagger, reloadTime))

View File

@@ -115,6 +115,11 @@ class Implant(HandledItem, ItemAttrShortcut):
copy.active = self.active copy.active = self.active
return copy return copy
def rebase(self, item):
active = self.active
Implant.__init__(self, item)
self.active = active
def __repr__(self): def __repr__(self):
return "Implant(ID={}, name={}) at {}".format( return "Implant(ID={}, name={}) at {}".format(
self.item.ID, self.item.name, hex(id(self)) self.item.ID, self.item.name, hex(id(self))

View File

@@ -23,9 +23,8 @@ from logbook import Logger
from sqlalchemy.orm import reconstructor, validates from sqlalchemy.orm import reconstructor, validates
import eos.db import eos.db
from eos.const import Slot from eos.const import FittingModuleState, FittingHardpoint, FittingSlot
from eos.effectHandlerHelpers import HandledCharge, HandledItem from eos.effectHandlerHelpers import HandledCharge, HandledItem
from eos.enum import Enum
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.saveddata.citadel import Citadel from eos.saveddata.citadel import Citadel
from eos.saveddata.mutator import Mutator from eos.saveddata.mutator import Mutator
@@ -35,44 +34,30 @@ from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
class State(Enum):
OFFLINE = -1
ONLINE = 0
ACTIVE = 1
OVERHEATED = 2
ProjectedMap = { ProjectedMap = {
State.OVERHEATED: State.ACTIVE, FittingModuleState.OVERHEATED: FittingModuleState.ACTIVE,
State.ACTIVE: State.OFFLINE, FittingModuleState.ACTIVE: FittingModuleState.OFFLINE,
State.OFFLINE: State.ACTIVE, FittingModuleState.OFFLINE: FittingModuleState.ACTIVE,
State.ONLINE: State.ACTIVE # Just in case FittingModuleState.ONLINE: FittingModuleState.ACTIVE # Just in case
} }
# Old state : New State # Old state : New State
LocalMap = { LocalMap = {
State.OVERHEATED: State.ACTIVE, FittingModuleState.OVERHEATED: FittingModuleState.ACTIVE,
State.ACTIVE: State.ONLINE, FittingModuleState.ACTIVE: FittingModuleState.ONLINE,
State.OFFLINE: State.ONLINE, FittingModuleState.OFFLINE: FittingModuleState.ONLINE,
State.ONLINE: State.ACTIVE FittingModuleState.ONLINE: FittingModuleState.ACTIVE
} }
# For system effects. They should only ever be online or offline # For system effects. They should only ever be online or offline
ProjectedSystem = { ProjectedSystem = {
State.OFFLINE: State.ONLINE, FittingModuleState.OFFLINE: FittingModuleState.ONLINE,
State.ONLINE: State.OFFLINE FittingModuleState.ONLINE: FittingModuleState.OFFLINE
} }
class Hardpoint(Enum):
NONE = 0
MISSILE = 1
TURRET = 2
class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"""An instance of this class represents a module together with its charge and modified attributes""" """An instance of this class represents a module together with its charge and modified attributes"""
MINING_ATTRIBUTES = ("miningAmount",) MINING_ATTRIBUTES = ("miningAmount",)
@@ -104,7 +89,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__charge = None self.__charge = None
self.projected = False self.projected = False
self.state = State.ONLINE self.state = FittingModuleState.ONLINE
self.build() self.build()
@reconstructor @reconstructor
@@ -153,7 +138,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__reloadTime = None self.__reloadTime = None
self.__reloadForce = None self.__reloadForce = None
self.__chargeCycles = None self.__chargeCycles = None
self.__hardpoint = Hardpoint.NONE self.__hardpoint = FittingHardpoint.NONE
self.__itemModifiedAttributes = ModifiedAttributeDict(parent=self) self.__itemModifiedAttributes = ModifiedAttributeDict(parent=self)
self.__chargeModifiedAttributes = ModifiedAttributeDict(parent=self) self.__chargeModifiedAttributes = ModifiedAttributeDict(parent=self)
self.__slot = self.dummySlot # defaults to None self.__slot = self.dummySlot # defaults to None
@@ -396,7 +381,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if self.isEmpty: if self.isEmpty:
self.__miningyield = 0 self.__miningyield = 0
else: else:
if self.state >= State.ACTIVE: if self.state >= FittingModuleState.ACTIVE:
volley = self.getModifiedItemAttr("specialtyMiningAmount") or self.getModifiedItemAttr( volley = self.getModifiedItemAttr("specialtyMiningAmount") or self.getModifiedItemAttr(
"miningAmount") or 0 "miningAmount") or 0
if volley: if volley:
@@ -410,7 +395,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return self.__miningyield return self.__miningyield
def getVolley(self, spoolOptions=None, targetResists=None, ignoreState=False): def getVolley(self, spoolOptions=None, targetResists=None, ignoreState=False):
if self.isEmpty or (self.state < State.ACTIVE and not ignoreState): if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
return DmgTypes(0, 0, 0, 0) return DmgTypes(0, 0, 0, 0)
if self.__baseVolley is None: if self.__baseVolley is None:
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
@@ -448,7 +433,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return dps return dps
def getRemoteReps(self, spoolOptions=None, ignoreState=False): def getRemoteReps(self, spoolOptions=None, ignoreState=False):
if self.isEmpty or (self.state < State.ACTIVE and not ignoreState): if self.isEmpty or (self.state < FittingModuleState.ACTIVE and not ignoreState):
return None, 0 return None, 0
def getBaseRemoteReps(module): def getBaseRemoteReps(module):
@@ -554,34 +539,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return fits return fits
def __fitRestrictions(self, fit, hardpointLimit=True): def __fitRestrictions(self, fit, hardpointLimit=True):
# Check ship type restrictions
fitsOnType = set()
fitsOnGroup = set()
shipType = self.getModifiedItemAttr("fitsToShipType", None) if not fit.canFit(self.item):
if shipType is not None:
fitsOnType.add(shipType)
for attr in list(self.itemModifiedAttributes.keys()):
if attr.startswith("canFitShipType"):
shipType = self.getModifiedItemAttr(attr, None)
if shipType is not None:
fitsOnType.add(shipType)
for attr in list(self.itemModifiedAttributes.keys()):
if attr.startswith("canFitShipGroup"):
shipGroup = self.getModifiedItemAttr(attr, None)
if shipGroup is not None:
fitsOnGroup.add(shipGroup)
if (len(fitsOnGroup) > 0 or len(fitsOnType) > 0) \
and fit.ship.item.group.ID not in fitsOnGroup \
and fit.ship.item.ID not in fitsOnType:
return False
# Citadel modules are now under a new category, so we can check this to ensure only structure modules can fit on a citadel
if isinstance(fit.ship, Citadel) and self.item.category.name != "Structure Module" or \
not isinstance(fit.ship, Citadel) and self.item.category.name == "Structure Module":
return False return False
# EVE doesn't let capital modules be fit onto subcapital hulls. Confirmed by CCP Larrikin that this is dictated # EVE doesn't let capital modules be fit onto subcapital hulls. Confirmed by CCP Larrikin that this is dictated
@@ -590,14 +549,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return False return False
# If the mod is a subsystem, don't let two subs in the same slot fit # If the mod is a subsystem, don't let two subs in the same slot fit
if self.slot == Slot.SUBSYSTEM: if self.slot == FittingSlot.SUBSYSTEM:
subSlot = self.getModifiedItemAttr("subSystemSlot") subSlot = self.getModifiedItemAttr("subSystemSlot")
for mod in fit.modules: for mod in fit.modules:
if mod.getModifiedItemAttr("subSystemSlot") == subSlot: if mod.getModifiedItemAttr("subSystemSlot") == subSlot:
return False return False
# Check rig sizes # Check rig sizes
if self.slot == Slot.RIG: if self.slot == FittingSlot.RIG:
if self.getModifiedItemAttr("rigSize") != fit.ship.getModifiedItemAttr("rigSize"): if self.getModifiedItemAttr("rigSize") != fit.ship.getModifiedItemAttr("rigSize"):
return False return False
@@ -627,9 +586,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# Check if we're within bounds # Check if we're within bounds
if state < -1 or state > 2: if state < -1 or state > 2:
return False return False
elif state >= State.ACTIVE and not self.item.isType("active"): elif state >= FittingModuleState.ACTIVE and not self.item.isType("active"):
return False return False
elif state == State.OVERHEATED and not self.item.isType("overheat"): elif state == FittingModuleState.OVERHEATED and not self.item.isType("overheat"):
return False return False
else: else:
return True return True
@@ -641,7 +600,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# If we're going to set module to offline or online for local modules or offline for projected, # If we're going to set module to offline or online for local modules or offline for projected,
# it should be fine for all cases # it should be fine for all cases
item = self.item item = self.item
if (state <= State.ONLINE and projectedOnto is None) or (state <= State.OFFLINE): if (state <= FittingModuleState.ONLINE and projectedOnto is None) or (state <= FittingModuleState.OFFLINE):
return True return True
# Check if the local module is over it's max limit; if it's not, we're fine # Check if the local module is over it's max limit; if it's not, we're fine
@@ -655,7 +614,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
group = item.group.name group = item.group.name
for mod in self.owner.modules: for mod in self.owner.modules:
currItem = getattr(mod, "item", None) currItem = getattr(mod, "item", None)
if mod.state >= State.ACTIVE and currItem is not None and currItem.group.name == group: if mod.state >= FittingModuleState.ACTIVE and currItem is not None and currItem.group.name == group:
currActive += 1 currActive += 1
if currActive > maxGroupActive: if currActive > maxGroupActive:
break break
@@ -718,28 +677,28 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@staticmethod @staticmethod
def __calculateHardpoint(item): def __calculateHardpoint(item):
effectHardpointMap = { effectHardpointMap = {
"turretFitted" : Hardpoint.TURRET, "turretFitted" : FittingHardpoint.TURRET,
"launcherFitted": Hardpoint.MISSILE "launcherFitted": FittingHardpoint.MISSILE
} }
if item is None: if item is None:
return Hardpoint.NONE return FittingHardpoint.NONE
for effectName, slot in effectHardpointMap.items(): for effectName, slot in effectHardpointMap.items():
if effectName in item.effects: if effectName in item.effects:
return slot return slot
return Hardpoint.NONE return FittingHardpoint.NONE
@staticmethod @staticmethod
def calculateSlot(item): def calculateSlot(item):
effectSlotMap = { effectSlotMap = {
"rigSlot" : Slot.RIG, "rigSlot" : FittingSlot.RIG.value,
"loPower" : Slot.LOW, "loPower" : FittingSlot.LOW.value,
"medPower" : Slot.MED, "medPower" : FittingSlot.MED.value,
"hiPower" : Slot.HIGH, "hiPower" : FittingSlot.HIGH.value,
"subSystem" : Slot.SUBSYSTEM, "subSystem" : FittingSlot.SUBSYSTEM.value,
"serviceSlot": Slot.SERVICE "serviceSlot": FittingSlot.SERVICE.value
} }
if item is None: if item is None:
return None return None
@@ -747,7 +706,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if effectName in item.effects: if effectName in item.effects:
return slot return slot
if item.group.name in Module.SYSTEM_GROUPS: if item.group.name in Module.SYSTEM_GROUPS:
return Slot.SYSTEM return FittingSlot.SYSTEM
return None return None
@@ -801,8 +760,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if effect.runTime == runTime and \ if effect.runTime == runTime and \
effect.activeByDefault and \ effect.activeByDefault and \
(effect.isType("offline") or (effect.isType("offline") or
(effect.isType("passive") and self.state >= State.ONLINE) or (effect.isType("passive") and self.state >= FittingModuleState.ONLINE) or
(effect.isType("active") and self.state >= State.ACTIVE)) and \ (effect.isType("active") and self.state >= FittingModuleState.ACTIVE)) and \
(not gang or (gang and effect.isType("gang"))): (not gang or (gang and effect.isType("gang"))):
chargeContext = ("moduleCharge",) chargeContext = ("moduleCharge",)
@@ -815,7 +774,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
effect.handler(fit, self, chargeContext) effect.handler(fit, self, chargeContext)
if self.item: if self.item:
if self.state >= State.OVERHEATED: if self.state >= FittingModuleState.OVERHEATED:
for effect in self.item.effects.values(): for effect in self.item.effects.values():
if effect.runTime == runTime and \ if effect.runTime == runTime and \
effect.isType("overheat") \ effect.isType("overheat") \
@@ -828,8 +787,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if effect.runTime == runTime and \ if effect.runTime == runTime and \
effect.activeByDefault and \ effect.activeByDefault and \
(effect.isType("offline") or (effect.isType("offline") or
(effect.isType("passive") and self.state >= State.ONLINE) or (effect.isType("passive") and self.state >= FittingModuleState.ONLINE) or
(effect.isType("active") and self.state >= State.ACTIVE)) \ (effect.isType("active") and self.state >= FittingModuleState.ACTIVE)) \
and ((projected and effect.isType("projected")) or not projected) \ and ((projected and effect.isType("projected")) or not projected) \
and ((gang and effect.isType("gang")) or not gang): and ((gang and effect.isType("gang")) or not gang):
try: try:
@@ -904,7 +863,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@property @property
def capUse(self): def capUse(self):
capNeed = self.getModifiedItemAttr("capacitorNeed") capNeed = self.getModifiedItemAttr("capacitorNeed")
if capNeed and self.state >= State.ACTIVE: if capNeed and self.state >= FittingModuleState.ACTIVE:
cycleTime = self.cycleTime cycleTime = self.cycleTime
if cycleTime > 0: if cycleTime > 0:
capUsed = capNeed / (cycleTime / 1000.0) capUsed = capNeed / (cycleTime / 1000.0)
@@ -916,10 +875,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def getProposedState(mod, click, proposedState=None): def getProposedState(mod, click, proposedState=None):
# todo: instead of passing in module, make this a instanced function. # todo: instead of passing in module, make this a instanced function.
pyfalog.debug("Get proposed state for module.") pyfalog.debug("Get proposed state for module.")
if mod.slot == Slot.SUBSYSTEM or mod.isEmpty: if mod.slot == FittingSlot.SUBSYSTEM or mod.isEmpty:
return State.ONLINE return FittingModuleState.ONLINE
if mod.slot == Slot.SYSTEM: if mod.slot == FittingSlot.SYSTEM:
transitionMap = ProjectedSystem transitionMap = ProjectedSystem
else: else:
transitionMap = ProjectedMap if mod.projected else LocalMap transitionMap = ProjectedMap if mod.projected else LocalMap
@@ -929,9 +888,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if proposedState is not None: if proposedState is not None:
state = proposedState state = proposedState
elif click == "right": elif click == "right":
state = State.OVERHEATED state = FittingModuleState.OVERHEATED
elif click == "ctrl": elif click == "ctrl":
state = State.OFFLINE state = FittingModuleState.OFFLINE
else: else:
state = transitionMap[currState] state = transitionMap[currState]
if not mod.isValidState(state): if not mod.isValidState(state):
@@ -956,6 +915,16 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return copy return copy
def rebase(self, item):
state = self.state
charge = self.charge
Module.__init__(self, item, self.baseItem, self.mutaplasmid)
self.state = state
if self.isValidCharge(charge):
self.charge = charge
for x in self.mutators.values():
Mutator(self, x.attribute, x.value)
def __repr__(self): def __repr__(self):
if self.item: if self.item:
return "Module(ID={}, name={}) at {}".format( return "Module(ID={}, name={}) at {}".format(

View File

@@ -19,41 +19,56 @@
# =============================================================================== # ===============================================================================
import time
from enum import IntEnum, unique from enum import IntEnum, unique
from time import time
from logbook import Logger from logbook import Logger
VALIDITY = 24 * 60 * 60 # Price validity period, 24 hours
REREQUEST = 4 * 60 * 60 # Re-request delay for failed fetches, 4 hours
TIMEOUT = 15 * 60 # Network timeout delay for connection issues, 15 minutes
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@unique @unique
class PriceStatus(IntEnum): class PriceStatus(IntEnum):
notFetched = 0 initialized = 0
success = 1 notSupported = 1
fail = 2 fetchSuccess = 2
notSupported = 3 fetchFail = 3
fetchTimeout = 4
class Price(object): class Price(object):
def __init__(self, typeID): def __init__(self, typeID):
self.typeID = typeID self.typeID = typeID
self.time = 0 self.time = 0
self.__price = 0 self.price = 0
self.status = PriceStatus.notFetched self.status = PriceStatus.initialized
@property def isValid(self, validityOverride=None):
def isValid(self): # Always attempt to update prices which were just initialized, and prices
return self.time >= time.time() # of unsupported items (maybe we start supporting them at some point)
if self.status in (PriceStatus.initialized, PriceStatus.notSupported):
@property return False
def price(self): elif self.status == PriceStatus.fetchSuccess:
if self.status != PriceStatus.success: return time() <= self.time + (validityOverride if validityOverride is not None else VALIDITY)
return 0 elif self.status == PriceStatus.fetchFail:
return time() <= self.time + REREQUEST
elif self.status == PriceStatus.fetchTimeout:
return time() <= self.time + TIMEOUT
else: else:
return self.__price or 0 return False
@price.setter def update(self, status, price=0):
def price(self, price): # Keep old price if we failed to fetch new one
self.__price = price if status in (PriceStatus.fetchFail, PriceStatus.fetchTimeout):
price = self.price
elif status != PriceStatus.fetchSuccess:
price = 0
self.time = time()
self.price = price
self.status = status

View File

@@ -19,21 +19,14 @@
from collections import namedtuple from collections import namedtuple
from enum import IntEnum, unique
from eos.const import SpoolType
from eos.utils.float import floatUnerr from eos.utils.float import floatUnerr
SpoolOptions = namedtuple('SpoolOptions', ('spoolType', 'spoolAmount', 'force')) SpoolOptions = namedtuple('SpoolOptions', ('spoolType', 'spoolAmount', 'force'))
@unique
class SpoolType(IntEnum):
SCALE = 0 # [0..1]
TIME = 1 # Expressed via time in seconds since spool up started
CYCLES = 2 # Expressed in amount of cycles since spool up started
def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAmount): def calculateSpoolup(modMaxValue, modStepValue, modCycleTime, spoolType, spoolAmount):
""" """
Calculate damage multiplier increment based on passed parameters. Module cycle time Calculate damage multiplier increment based on passed parameters. Module cycle time

BIN
eve.db

Binary file not shown.

View File

@@ -110,6 +110,9 @@ class BoosterView(d.Display):
self.origional = fit.boosters if fit is not None else None self.origional = fit.boosters if fit is not None else None
self.boosters = stuff = fit.boosters[:] if fit is not None else None self.boosters = stuff = fit.boosters[:] if fit is not None else None
if stuff is not None:
stuff.sort(key=lambda booster: booster.slot or 0)
if event.fitID != self.lastFitId: if event.fitID != self.lastFitId:
self.lastFitId = event.fitID self.lastFitId = event.fitID

View File

@@ -81,7 +81,9 @@ class CargoView(d.Display):
if data[0] == "fitting": if data[0] == "fitting":
self.swapModule(x, y, int(data[1])) self.swapModule(x, y, int(data[1]))
elif data[0] == "market": elif data[0] == "market":
self.mainFrame.command.Submit(cmd.GuiAddCargoCommand(self.mainFrame.getActiveFit(), int(data[1]))) fit = self.mainFrame.getActiveFit()
if fit:
self.mainFrame.command.Submit(cmd.GuiAddCargoCommand(fit, int(data[1])))
def startDrag(self, event): def startDrag(self, event):
row = event.GetIndex() row = event.GetIndex()

View File

@@ -25,7 +25,7 @@ from gui.builtinMarketBrowser.events import ItemSelected, ITEM_SELECTED
import gui.mainFrame import gui.mainFrame
import gui.display as d import gui.display as d
from gui.builtinViewColumns.state import State from gui.builtinViewColumns.state import State
from eos.saveddata.module import Slot from eos.const import FittingSlot
from gui.contextMenu import ContextMenu from gui.contextMenu import ContextMenu
from gui.utils.staticHelpers import DragDropHelper from gui.utils.staticHelpers import DragDropHelper
from service.fit import Fit from service.fit import Fit
@@ -93,9 +93,9 @@ class FighterView(wx.Panel):
if fit: if fit:
for x in self.labels: for x in self.labels:
if fit.isStructure: if fit.isStructure:
slot = getattr(Slot, "FS_{}".format(x.upper())) slot = getattr(FittingSlot, "FS_{}".format(x.upper()))
else: else:
slot = getattr(Slot, "F_{}".format(x.upper())) slot = getattr(FittingSlot, "F_{}".format(x.upper()))
used = fit.getSlotsUsed(slot) used = fit.getSlotsUsed(slot)
total = fit.getNumSlots(slot) total = fit.getNumSlots(slot)
color = wx.Colour(204, 51, 51) if used > total else wx.SystemSettings.GetColour( color = wx.Colour(204, 51, 51) if used > total else wx.SystemSettings.GetColour(
@@ -122,8 +122,8 @@ class FighterDisplay(d.Display):
# "Max Range", # "Max Range",
# "Miscellanea", # "Miscellanea",
"attr:maxVelocity", "attr:maxVelocity",
"Fighter Abilities" "Fighter Abilities",
# "Price", "Price",
] ]
def __init__(self, parent): def __init__(self, parent):

View File

@@ -26,7 +26,7 @@ from gui.builtinViewColumns.state import State
from gui.utils.staticHelpers import DragDropHelper from gui.utils.staticHelpers import DragDropHelper
from gui.contextMenu import ContextMenu from gui.contextMenu import ContextMenu
import gui.globalEvents as GE import gui.globalEvents as GE
from eos.saveddata.fit import ImplantLocation from eos.const import ImplantLocation
from service.fit import Fit from service.fit import Fit
from service.market import Market from service.market import Market
import gui.fitCommands as cmd import gui.fitCommands as cmd
@@ -204,7 +204,10 @@ class ImplantDisplay(d.Display):
def removeImplant(self, implant): def removeImplant(self, implant):
fitID = self.mainFrame.getActiveFit() fitID = self.mainFrame.getActiveFit()
self.mainFrame.command.Submit(cmd.GuiRemoveImplantCommand(fitID, self.original.index(implant))) sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit.implantLocation == ImplantLocation.FIT:
self.mainFrame.command.Submit(cmd.GuiRemoveImplantCommand(fitID, self.original.index(implant)))
def click(self, event): def click(self, event):
event.Skip() event.Skip()

View File

@@ -8,6 +8,7 @@ import gui.mainFrame
from gui.contextMenu import ContextMenu from gui.contextMenu import ContextMenu
from service.market import Market from service.market import Market
from service.settings import ContextMenuSettings from service.settings import ContextMenuSettings
from service.fit import Fit
class MetaSwap(ContextMenu): class MetaSwap(ContextMenu):
@@ -53,6 +54,8 @@ class MetaSwap(ContextMenu):
def getSubMenu(self, context, selection, rootMenu, i, pitem): def getSubMenu(self, context, selection, rootMenu, i, pitem):
self.moduleLookup = {} self.moduleLookup = {}
sFit = Fit.getInstance()
fit = sFit.getFit(self.mainFrame.getActiveFit())
def get_metalevel(x): def get_metalevel(x):
if "metaLevel" not in x.attributes: if "metaLevel" not in x.attributes:
@@ -114,6 +117,7 @@ class MetaSwap(ContextMenu):
id = ContextMenu.nextID() id = ContextMenu.nextID()
mitem = wx.MenuItem(rootMenu, id, item.name) mitem = wx.MenuItem(rootMenu, id, item.name)
mitem.Enable(fit.canFit(item))
bindmenu.Bind(wx.EVT_MENU, self.handleModule, mitem) bindmenu.Bind(wx.EVT_MENU, self.handleModule, mitem)
self.moduleLookup[id] = item, context self.moduleLookup[id] = item, context

View File

@@ -5,7 +5,7 @@ import wx
import gui.fitCommands as cmd import gui.fitCommands as cmd
import gui.mainFrame import gui.mainFrame
from eos.saveddata.module import Hardpoint from eos.const import FittingHardpoint
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.contextMenu import ContextMenu from gui.contextMenu import ContextMenu
from service.market import Market from service.market import Market
@@ -136,7 +136,7 @@ class ModuleAmmoPicker(ContextMenu):
hardpoint = self.module.hardpoint hardpoint = self.module.hardpoint
moduleName = self.module.item.name moduleName = self.module.item.name
# Make sure we do not consider mining turrets as combat turrets # Make sure we do not consider mining turrets as combat turrets
if hardpoint == Hardpoint.TURRET and self.module.getModifiedItemAttr("miningAmount", None) is None: if hardpoint == FittingHardpoint.TURRET and self.module.getModifiedItemAttr("miningAmount", None) is None:
self.addSeperator(m, "Long Range") self.addSeperator(m, "Long Range")
items = [] items = []
range_ = None range_ = None
@@ -180,7 +180,7 @@ class ModuleAmmoPicker(ContextMenu):
m.Append(item) m.Append(item)
self.addSeperator(m, "Short Range") self.addSeperator(m, "Short Range")
elif hardpoint == Hardpoint.MISSILE and moduleName != 'Festival Launcher': elif hardpoint == FittingHardpoint.MISSILE and moduleName != 'Festival Launcher':
self.charges.sort(key=self.missileSorter) self.charges.sort(key=self.missileSorter)
type_ = None type_ = None
sub = None sub = None

View File

@@ -0,0 +1,82 @@
# noinspection PyPackageRequirements
import wx
import eos.config
import gui.mainFrame
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from gui import globalEvents as GE
from gui.contextMenu import ContextMenu
from service.settings import ContextMenuSettings
from service.fit import Fit
class SpoolUp(ContextMenu):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.settings = ContextMenuSettings.getInstance()
self.cycleMap = {}
self.resetId = None
def display(self, srcContext, selection):
if not self.settings.get('spoolup'):
return False
if srcContext not in ("fittingModule") or self.mainFrame.getActiveFit() is None:
return False
self.mod = selection[0]
return self.mod.item.group.name in ("Precursor Weapon", "Mutadaptive Remote Armor Repairer")
def getText(self, itmContext, selection):
return "Spoolup Cycles"
def getSubMenu(self, context, selection, rootMenu, i, pitem):
m = wx.Menu()
if "wxMSW" in wx.PlatformInfo:
bindmenu = rootMenu
else:
bindmenu = m
isNotDefault = self.mod.spoolType is not None and self.mod.spoolAmount is not None
cycleDefault = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, eos.config.settings['globalDefaultSpoolupPercentage'], True))[0]
cycleCurrent = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, eos.config.settings['globalDefaultSpoolupPercentage'], False))[0]
cycleMin = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, 0, True))[0]
cycleMax = self.mod.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, 1, True))[0]
for cycle in range(cycleMin, cycleMax + 1):
menuId = ContextMenu.nextID()
# Show default only for current value and when not overriden
if not isNotDefault and cycle == cycleDefault:
text = "{} (default)".format(cycle)
else:
text = "{}".format(cycle)
item = wx.MenuItem(m, menuId, text, kind=wx.ITEM_CHECK)
bindmenu.Bind(wx.EVT_MENU, self.handleSpoolChange, item)
m.Append(item)
item.Check(isNotDefault and cycle == cycleCurrent)
self.cycleMap[menuId] = cycle
self.resetId = ContextMenu.nextID()
item = wx.MenuItem(m, self.resetId, "Reset")
bindmenu.Bind(wx.EVT_MENU, self.handleSpoolChange, item)
m.Append(item)
return m
def handleSpoolChange(self, event):
if event.Id == self.resetId:
self.mod.spoolType = None
self.mod.spoolAmount = None
elif event.Id in self.cycleMap:
cycles = self.cycleMap[event.Id]
self.mod.spoolType = SpoolType.CYCLES
self.mod.spoolAmount = cycles
fitID = self.mainFrame.getActiveFit()
Fit.getInstance().recalc(fitID)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID))
SpoolUp.register()

View File

@@ -1,22 +1,4 @@
from enum import Enum, auto from service.const import GuiAttrGroup
# Define the various groups of attributes
class AttrGroup(Enum):
FITTING = auto()
STRUCTURE = auto()
SHIELD = auto()
ARMOR = auto()
TARGETING = auto()
EWAR_RESISTS = auto()
CAPACITOR = auto()
SHARED_FACILITIES = auto()
FIGHTER_FACILITIES = auto()
ON_DEATH = auto()
JUMP_SYSTEMS = auto()
PROPULSIONS = auto()
FIGHTERS = auto()
RequiredSkillAttrs = sum((["requiredSkill{}".format(x), "requiredSkill{}Level".format(x)] for x in range(1, 7)), []) RequiredSkillAttrs = sum((["requiredSkill{}".format(x), "requiredSkill{}Level".format(x)] for x in range(1, 7)), [])
@@ -45,7 +27,7 @@ for x in AttrGroups:
# Start defining all the known attribute groups # Start defining all the known attribute groups
AttrGroupDict = { AttrGroupDict = {
AttrGroup.FITTING : { GuiAttrGroup.FITTING : {
"label" : "Fitting", "label" : "Fitting",
"attributes": [ "attributes": [
# parent-level attributes # parent-level attributes
@@ -67,7 +49,7 @@ AttrGroupDict = {
# "mass", # "mass",
] ]
}, },
AttrGroup.STRUCTURE : { GuiAttrGroup.STRUCTURE : {
"label" : "Structure", "label" : "Structure",
"attributes": [ "attributes": [
"hp", "hp",
@@ -97,7 +79,7 @@ AttrGroupDict = {
"explosiveDamageResonance" "explosiveDamageResonance"
] ]
}, },
AttrGroup.ARMOR : { GuiAttrGroup.ARMOR : {
"label": "Armor", "label": "Armor",
"attributes":[ "attributes":[
"armorHP", "armorHP",
@@ -109,7 +91,7 @@ AttrGroupDict = {
] ]
}, },
AttrGroup.SHIELD : { GuiAttrGroup.SHIELD : {
"label": "Shield", "label": "Shield",
"attributes": [ "attributes": [
"shieldCapacity", "shieldCapacity",
@@ -122,7 +104,7 @@ AttrGroupDict = {
] ]
}, },
AttrGroup.EWAR_RESISTS : { GuiAttrGroup.EWAR_RESISTS : {
"label": "Electronic Warfare", "label": "Electronic Warfare",
"attributes": [ "attributes": [
"ECMResistance", "ECMResistance",
@@ -135,14 +117,14 @@ AttrGroupDict = {
"weaponDisruptionResistance", "weaponDisruptionResistance",
] ]
}, },
AttrGroup.CAPACITOR : { GuiAttrGroup.CAPACITOR : {
"label": "Capacitor", "label": "Capacitor",
"attributes": [ "attributes": [
"capacitorCapacity", "capacitorCapacity",
"rechargeRate", "rechargeRate",
] ]
}, },
AttrGroup.TARGETING : { GuiAttrGroup.TARGETING : {
"label": "Targeting", "label": "Targeting",
"attributes": [ "attributes": [
"maxTargetRange", "maxTargetRange",
@@ -160,7 +142,7 @@ AttrGroupDict = {
"scanLadarStrength", "scanLadarStrength",
] ]
}, },
AttrGroup.SHARED_FACILITIES : { GuiAttrGroup.SHARED_FACILITIES : {
"label" : "Shared Facilities", "label" : "Shared Facilities",
"attributes": [ "attributes": [
"fleetHangarCapacity", "fleetHangarCapacity",
@@ -168,7 +150,7 @@ AttrGroupDict = {
"maxJumpClones", "maxJumpClones",
] ]
}, },
AttrGroup.FIGHTER_FACILITIES: { GuiAttrGroup.FIGHTER_FACILITIES: {
"label": "Fighter Squadron Facilities", "label": "Fighter Squadron Facilities",
"attributes": [ "attributes": [
"fighterCapacity", "fighterCapacity",
@@ -181,7 +163,7 @@ AttrGroupDict = {
"fighterStandupHeavySlots", "fighterStandupHeavySlots",
] ]
}, },
AttrGroup.ON_DEATH : { GuiAttrGroup.ON_DEATH : {
"label": "On Death", "label": "On Death",
"attributes": [ "attributes": [
"onDeathDamageEM", "onDeathDamageEM",
@@ -192,7 +174,7 @@ AttrGroupDict = {
"onDeathSignatureRadius", "onDeathSignatureRadius",
] ]
}, },
AttrGroup.JUMP_SYSTEMS : { GuiAttrGroup.JUMP_SYSTEMS : {
"label": "Jump Drive Systems", "label": "Jump Drive Systems",
"attributes": [ "attributes": [
"jumpDriveCapacitorNeed", "jumpDriveCapacitorNeed",
@@ -206,13 +188,13 @@ AttrGroupDict = {
"jumpPortalDuration", "jumpPortalDuration",
] ]
}, },
AttrGroup.PROPULSIONS : { GuiAttrGroup.PROPULSIONS : {
"label": "Propulsion", "label": "Propulsion",
"attributes": [ "attributes": [
"maxVelocity" "maxVelocity"
] ]
}, },
AttrGroup.FIGHTERS : { GuiAttrGroup.FIGHTERS : {
"label": "Fighter", "label": "Fighter",
"attributes": [ "attributes": [
"mass", "mass",
@@ -225,28 +207,36 @@ AttrGroupDict = {
"fighterSquadronOrbitRange", "fighterSquadronOrbitRange",
] ]
}, },
GuiAttrGroup.SHIP_GROUP : {
"label" : "Can Fit To",
"attributes": []
},
} }
AttrGroupDict[GuiAttrGroup.SHIP_GROUP]["attributes"].extend([("canFitShipGroup{:02d}".format(i+1), "Group") for i in range(20)])
AttrGroupDict[GuiAttrGroup.SHIP_GROUP]["attributes"].extend([("canFitShipType{:01d}".format(i+1), "Ship") for i in range(20)])
Group1 = [ Group1 = [
AttrGroup.FITTING, GuiAttrGroup.FITTING,
AttrGroup.STRUCTURE, GuiAttrGroup.STRUCTURE,
AttrGroup.ARMOR, GuiAttrGroup.ARMOR,
AttrGroup.SHIELD, GuiAttrGroup.SHIELD,
AttrGroup.EWAR_RESISTS, GuiAttrGroup.EWAR_RESISTS,
AttrGroup.CAPACITOR, GuiAttrGroup.CAPACITOR,
AttrGroup.TARGETING, GuiAttrGroup.TARGETING,
AttrGroup.SHARED_FACILITIES, GuiAttrGroup.SHARED_FACILITIES,
AttrGroup.FIGHTER_FACILITIES, GuiAttrGroup.FIGHTER_FACILITIES,
AttrGroup.ON_DEATH, GuiAttrGroup.ON_DEATH,
AttrGroup.JUMP_SYSTEMS, GuiAttrGroup.JUMP_SYSTEMS,
AttrGroup.PROPULSIONS, GuiAttrGroup.PROPULSIONS,
GuiAttrGroup.SHIP_GROUP
] ]
CategoryGroups = { CategoryGroups = {
"Fighter" : [ "Fighter" : [
AttrGroup.FIGHTERS, GuiAttrGroup.FIGHTERS,
AttrGroup.SHIELD, GuiAttrGroup.SHIELD,
AttrGroup.TARGETING, GuiAttrGroup.TARGETING,
], ],
"Ship" : Group1, "Ship" : Group1,
"Drone" : Group1, "Drone" : Group1,

View File

@@ -4,12 +4,12 @@ import config
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import wx import wx
import wx.lib.agw.hypertreelist import wx.lib.agw.hypertreelist
from gui.builtinItemStatsViews.helpers import AutoListCtrl
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount, roundDec from gui.utils.numberFormatter import formatAmount, roundDec
from enum import IntEnum from enum import IntEnum
from gui.builtinItemStatsViews.attributeGrouping import * from gui.builtinItemStatsViews.attributeGrouping import *
from service.const import GuiAttrGroup
class AttributeView(IntEnum): class AttributeView(IntEnum):
@@ -19,7 +19,8 @@ class AttributeView(IntEnum):
class ItemParams(wx.Panel): class ItemParams(wx.Panel):
def __init__(self, parent, stuff, item, context=None): def __init__(self, parent, stuff, item, context=None):
wx.Panel.__init__(self, parent) # Had to manually set the size here, otherwise column widths couldn't be calculated correctly. See #1878
wx.Panel.__init__(self, parent, size=(1000, 1000))
mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer = wx.BoxSizer(wx.VERTICAL)
self.paramList = wx.lib.agw.hypertreelist.HyperTreeList(self, wx.ID_ANY, agwStyle=wx.TR_HIDE_ROOT | wx.TR_NO_LINES | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_HAS_BUTTONS) self.paramList = wx.lib.agw.hypertreelist.HyperTreeList(self, wx.ID_ANY, agwStyle=wx.TR_HIDE_ROOT | wx.TR_NO_LINES | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_HAS_BUTTONS)
@@ -28,7 +29,7 @@ class ItemParams(wx.Panel):
mainSizer.Add(self.paramList, 1, wx.ALL | wx.EXPAND, 0) mainSizer.Add(self.paramList, 1, wx.ALL | wx.EXPAND, 0)
self.SetSizer(mainSizer) self.SetSizer(mainSizer)
self.toggleView = 1 self.toggleView = AttributeView.NORMAL
self.stuff = stuff self.stuff = stuff
self.item = item self.item = item
self.attrInfo = {} self.attrInfo = {}
@@ -40,9 +41,6 @@ class ItemParams(wx.Panel):
if self.stuff is not None: if self.stuff is not None:
self.paramList.AddColumn("Base Value") self.paramList.AddColumn("Base Value")
self.paramList.SetMainColumn(0) # the one with the tree in it...
self.paramList.SetColumnWidth(0, 300)
self.m_staticline = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) self.m_staticline = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL)
mainSizer.Add(self.m_staticline, 0, wx.EXPAND) mainSizer.Add(self.m_staticline, 0, wx.EXPAND)
bSizer = wx.BoxSizer(wx.HORIZONTAL) bSizer = wx.BoxSizer(wx.HORIZONTAL)
@@ -62,6 +60,8 @@ class ItemParams(wx.Panel):
mainSizer.Add(bSizer, 0, wx.ALIGN_RIGHT) mainSizer.Add(bSizer, 0, wx.ALIGN_RIGHT)
self.imageList = wx.ImageList(16, 16)
self.PopulateList() self.PopulateList()
self.toggleViewBtn.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleViewMode) self.toggleViewBtn.Bind(wx.EVT_TOGGLEBUTTON, self.ToggleViewMode)
@@ -164,10 +164,24 @@ class ItemParams(wx.Panel):
] ]
) )
def SetupImageList(self):
self.imageList.RemoveAll()
self.blank_icon = self.imageList.Add(BitmapLoader.getBitmap("transparent16x16", "gui"))
self.unknown_icon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons"))
self.paramList.AssignImageList(self.imageList)
def AddAttribute(self, parent, attr): def AddAttribute(self, parent, attr):
display = None
if isinstance(attr, tuple):
display = attr[1]
attr = attr[0]
if attr in self.attrValues and attr not in self.processed_attribs: if attr in self.attrValues and attr not in self.processed_attribs:
data = self.GetData(attr) data = self.GetData(attr, display)
if data is None: if data is None:
return return
@@ -188,14 +202,14 @@ class ItemParams(wx.Panel):
def PopulateList(self): def PopulateList(self):
# self.paramList.setResizeColumn(0) # self.paramList.setResizeColumn(0)
self.imageList = wx.ImageList(16, 16) self.SetupImageList()
self.processed_attribs = set() self.processed_attribs = set()
root = self.paramList.AddRoot("The Root Item") root = self.paramList.AddRoot("The Root Item")
misc_parent = root misc_parent = root
# We must first deet4ermine if it's categorey already has defined groupings set for it. Otherwise, we default to just using the fitting group # We must first deet4ermine if it's categorey already has defined groupings set for it. Otherwise, we default to just using the fitting group
order = CategoryGroups.get(self.item.category.categoryName, [AttrGroup.FITTING]) order = CategoryGroups.get(self.item.category.categoryName, [GuiAttrGroup.FITTING, GuiAttrGroup.SHIP_GROUP])
# start building out the tree # start building out the tree
for data in [AttrGroupDict[o] for o in order]: for data in [AttrGroupDict[o] for o in order]:
heading = data.get("label") heading = data.get("label")
@@ -243,10 +257,14 @@ class ItemParams(wx.Panel):
self.AddAttribute(root, name) self.AddAttribute(root, name)
self.paramList.AssignImageList(self.imageList)
self.Layout() self.Layout()
for i in range(self.paramList.GetMainWindow().GetColumnCount()):
self.paramList.SetColumnWidth(i, wx.LIST_AUTOSIZE)
def GetData(self, attr): def GetData(self, attr):
def GetData(self, attr, displayOveride = None):
info = self.attrInfo.get(attr) info = self.attrInfo.get(attr)
att = self.attrValues[attr] att = self.attrValues[attr]
@@ -264,8 +282,8 @@ class ItemParams(wx.Panel):
if self.toggleView == AttributeView.NORMAL and ((attr not in GroupedAttributes and not value) or info is None or not info.published or attr in RequiredSkillAttrs): if self.toggleView == AttributeView.NORMAL and ((attr not in GroupedAttributes and not value) or info is None or not info.published or attr in RequiredSkillAttrs):
return None return None
if info and info.displayName and self.toggleView == 1: if info and info.displayName and self.toggleView == AttributeView.NORMAL:
attrName = info.displayName attrName = displayOveride or info.displayName
else: else:
attrName = attr attrName = attr
@@ -278,27 +296,27 @@ class ItemParams(wx.Panel):
icon = BitmapLoader.getBitmap(iconFile, "icons") icon = BitmapLoader.getBitmap(iconFile, "icons")
if icon is None: if icon is None:
icon = BitmapLoader.getBitmap("transparent16x16", "gui") attrIcon = self.blank_icon
else:
attrIcon = self.imageList.Add(icon) attrIcon = self.imageList.Add(icon)
else: else:
attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) attrIcon = self.unknown_icon
else: else:
attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons")) attrIcon = self.unknown_icon
# index = self.paramList.AppendItem(root, attrName) # index = self.paramList.AppendItem(root, attrName)
# idNameMap[idCount] = attrName # idNameMap[idCount] = attrName
# self.paramList.SetPyData(index, idCount) # self.paramList.SetPyData(index, idCount)
# idCount += 1 # idCount += 1
if self.toggleView != 1: if self.toggleView == AttributeView.RAW:
valueUnit = str(value) valueUnit = str(value)
elif info and info.unit: elif info and info.unit:
valueUnit = self.FormatValue(*info.unit.PreformatValue(value)) valueUnit = self.FormatValue(*info.unit.PreformatValue(value))
else: else:
valueUnit = formatAmount(value, 3, 0, 0) valueUnit = formatAmount(value, 3, 0, 0)
if self.toggleView != 1: if self.toggleView == AttributeView.RAW:
valueUnitDefault = str(valueDefault) valueUnitDefault = str(valueDefault)
elif info and info.unit: elif info and info.unit:
valueUnitDefault = self.FormatValue(*info.unit.PreformatValue(valueDefault)) valueUnitDefault = self.FormatValue(*info.unit.PreformatValue(valueDefault))
@@ -346,6 +364,7 @@ if __name__ == "__main__":
#item = eos.db.getItem(526) # Stasis Webifier I #item = eos.db.getItem(526) # Stasis Webifier I
item = eos.db.getItem(486) # 200mm AutoCannon I item = eos.db.getItem(486) # 200mm AutoCannon I
#item = eos.db.getItem(200) # Phased Plasma L #item = eos.db.getItem(200) # Phased Plasma L
super().__init__(None, title="Test Attribute Window | {} - {}".format(item.ID, item.name), size=(1000, 500)) super().__init__(None, title="Test Attribute Window | {} - {}".format(item.ID, item.name), size=(1000, 500))
if 'wxMSW' in wx.PlatformInfo: if 'wxMSW' in wx.PlatformInfo:
@@ -359,6 +378,7 @@ if __name__ == "__main__":
main_sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 2) main_sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 2)
self.SetSizer(main_sizer) self.SetSizer(main_sizer)
self.Layout()
app = wx.App(redirect=False) # Error messages go to popup window app = wx.App(redirect=False) # Error messages go to popup window
top = Frame() top = Frame()

View File

@@ -16,7 +16,7 @@ class ItemCompare(wx.Panel):
def __init__(self, parent, stuff, item, items, context=None): def __init__(self, parent, stuff, item, items, context=None):
# Start dealing with Price stuff to get that thread going # Start dealing with Price stuff to get that thread going
sPrice = ServicePrice.getInstance() sPrice = ServicePrice.getInstance()
sPrice.getPrices(items, self.UpdateList) sPrice.getPrices(items, self.UpdateList, fetchTimeout=90)
wx.Panel.__init__(self, parent) wx.Panel.__init__(self, parent)
mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer = wx.BoxSizer(wx.VERTICAL)

View File

@@ -43,8 +43,12 @@ class ItemMutator(wx.Panel):
self.badColor = wx.Colour(255, 64, 0) self.badColor = wx.Colour(255, 64, 0)
self.event_mapping = {} self.event_mapping = {}
higOverrides = {
('Stasis Web', 'speedFactor'): False,
}
for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName): for m in sorted(stuff.mutators.values(), key=lambda x: x.attribute.displayName):
highIsGood = higOverrides.get((stuff.item.group.name, m.attribute.name), m.highIsGood)
# Format: [raw value, modifier applied to base raw value, display value] # Format: [raw value, modifier applied to base raw value, display value]
range1 = (m.minValue, m.attribute.unit.SimplifyValue(m.minValue)) range1 = (m.minValue, m.attribute.unit.SimplifyValue(m.minValue))
range2 = (m.maxValue, m.attribute.unit.SimplifyValue(m.maxValue)) range2 = (m.maxValue, m.attribute.unit.SimplifyValue(m.maxValue))
@@ -59,7 +63,7 @@ class ItemMutator(wx.Panel):
minRange = range2 minRange = range2
maxRange = range1 maxRange = range1
if (m.highIsGood and minRange[0] >= maxRange[0]) or (not m.highIsGood and minRange[0] <= maxRange[0]): if (highIsGood and minRange[0] >= maxRange[0]) or (not highIsGood and minRange[0] <= maxRange[0]):
betterRange = minRange betterRange = minRange
worseRange = maxRange worseRange = maxRange
else: else:

View File

@@ -36,7 +36,7 @@ class PFContextMenuPref(PreferenceView):
self.rbBox1 = wx.RadioBox(panel, -1, "Set as Damage Pattern", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) self.rbBox1 = wx.RadioBox(panel, -1, "Set as Damage Pattern", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox1.SetSelection(self.settings.get('ammoPattern')) self.rbBox1.SetSelection(self.settings.get('ammoPattern'))
rbSizerRow1.Add(self.rbBox1, 1, wx.TOP | wx.RIGHT, 5) rbSizerRow1.Add(self.rbBox1, 1, wx.ALL, 5)
self.rbBox1.Bind(wx.EVT_RADIOBOX, self.OnSetting1Change) self.rbBox1.Bind(wx.EVT_RADIOBOX, self.OnSetting1Change)
self.rbBox2 = wx.RadioBox(panel, -1, "Change Skills", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) self.rbBox2 = wx.RadioBox(panel, -1, "Change Skills", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
@@ -56,36 +56,39 @@ class PFContextMenuPref(PreferenceView):
self.rbBox4 = wx.RadioBox(panel, -1, "Variations", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) self.rbBox4 = wx.RadioBox(panel, -1, "Variations", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox4.SetSelection(self.settings.get('metaSwap')) self.rbBox4.SetSelection(self.settings.get('metaSwap'))
rbSizerRow2.Add(self.rbBox4, 1, wx.TOP | wx.RIGHT, 5) rbSizerRow2.Add(self.rbBox4, 1, wx.ALL, 5)
self.rbBox4.Bind(wx.EVT_RADIOBOX, self.OnSetting4Change) self.rbBox4.Bind(wx.EVT_RADIOBOX, self.OnSetting4Change)
''' # self.rbBox5 = wx.RadioBox(panel, -1, "Charge", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox5 = wx.RadioBox(panel, -1, "Charge", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) # self.rbBox5.SetSelection(self.settings.get('moduleAmmoPicker'))
self.rbBox5.SetSelection(self.settings.get('moduleAmmoPicker')) # rbSizerRow2.Add(self.rbBox5, 0, wx.ALL, 5)
rbSizerRow2.Add(self.rbBox5, 1, wx.ALL, 5) # self.rbBox5.Bind(wx.EVT_RADIOBOX, self.OnSetting5Change)
self.rbBox5.Bind(wx.EVT_RADIOBOX, self.OnSetting5Change)
'''
self.rbBox6 = wx.RadioBox(panel, -1, "Charge (All)", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) self.rbBox6 = wx.RadioBox(panel, -1, "Charge (All)", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox6.SetSelection(self.settings.get('moduleGlobalAmmoPicker')) self.rbBox6.SetSelection(self.settings.get('moduleGlobalAmmoPicker'))
rbSizerRow2.Add(self.rbBox6, 1, wx.ALL, 5) rbSizerRow2.Add(self.rbBox6, 1, wx.ALL, 5)
self.rbBox6.Bind(wx.EVT_RADIOBOX, self.OnSetting6Change) self.rbBox6.Bind(wx.EVT_RADIOBOX, self.OnSetting6Change)
self.rbBox7 = wx.RadioBox(panel, -1, "Project onto Fit", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox7.SetSelection(self.settings.get('project'))
rbSizerRow2.Add(self.rbBox7, 1, wx.ALL, 5)
self.rbBox7.Bind(wx.EVT_RADIOBOX, self.OnSetting7Change)
mainSizer.Add(rbSizerRow2, 1, wx.ALL | wx.EXPAND, 0) mainSizer.Add(rbSizerRow2, 1, wx.ALL | wx.EXPAND, 0)
# Row 3 # Row 3
rbSizerRow3 = wx.BoxSizer(wx.HORIZONTAL) rbSizerRow3 = wx.BoxSizer(wx.HORIZONTAL)
self.rbBox7 = wx.RadioBox(panel, -1, "Project onto Fit", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox7.SetSelection(self.settings.get('project'))
rbSizerRow3.Add(self.rbBox7, 1, wx.TOP | wx.RIGHT, 5)
self.rbBox7.Bind(wx.EVT_RADIOBOX, self.OnSetting7Change)
self.rbBox8 = wx.RadioBox(panel, -1, "Fill with module", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS) self.rbBox8 = wx.RadioBox(panel, -1, "Fill with module", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox8.SetSelection(self.settings.get('moduleFill')) self.rbBox8.SetSelection(self.settings.get('moduleFill'))
rbSizerRow3.Add(self.rbBox8, 1, wx.TOP | wx.RIGHT, 5) rbSizerRow3.Add(self.rbBox8, 1, wx.ALL, 5)
self.rbBox8.Bind(wx.EVT_RADIOBOX, self.OnSetting8Change) self.rbBox8.Bind(wx.EVT_RADIOBOX, self.OnSetting8Change)
self.rbBox9 = wx.RadioBox(panel, -1, "Spoolup", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox9.SetSelection(self.settings.get('spoolup'))
rbSizerRow3.Add(self.rbBox9, 1, wx.ALL, 5)
self.rbBox9.Bind(wx.EVT_RADIOBOX, self.OnSetting9Change)
mainSizer.Add(rbSizerRow3, 1, wx.ALL | wx.EXPAND, 0) mainSizer.Add(rbSizerRow3, 1, wx.ALL | wx.EXPAND, 0)
panel.SetSizer(mainSizer) panel.SetSizer(mainSizer)
@@ -115,6 +118,9 @@ class PFContextMenuPref(PreferenceView):
def OnSetting8Change(self, event): def OnSetting8Change(self, event):
self.settings.set('moduleFill', event.GetInt()) self.settings.set('moduleFill', event.GetInt())
def OnSetting9Change(self, event):
self.settings.set('spoolup', event.GetInt())
def getImage(self): def getImage(self):
return BitmapLoader.getBitmap("settings_menu", "gui") return BitmapLoader.getBitmap("settings_menu", "gui")

View File

@@ -8,6 +8,7 @@ import gui.globalEvents as GE
from gui.preferenceView import PreferenceView from gui.preferenceView import PreferenceView
from service.settings import EOSSettings from service.settings import EOSSettings
import gui.mainFrame import gui.mainFrame
from wx.lib.intctrl import IntCtrl
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -61,6 +62,22 @@ class PFFittingEnginePref(PreferenceView):
wx.DefaultPosition, wx.DefaultSize, 0) wx.DefaultPosition, wx.DefaultSize, 0)
mainSizer.Add(self.cbUniversalAdaptiveArmorHardener, 0, wx.ALL | wx.EXPAND, 5) mainSizer.Add(self.cbUniversalAdaptiveArmorHardener, 0, wx.ALL | wx.EXPAND, 5)
spoolup_sizer = wx.BoxSizer(wx.HORIZONTAL)
self.spool_up_label = wx.StaticText(panel, wx.ID_ANY, "Global Default Spoolup Percentage:", wx.DefaultPosition, wx.DefaultSize, 0)
self.spool_up_label.Wrap(-1)
self.spool_up_label.SetCursor(helpCursor)
self.spool_up_label.SetToolTip(
wx.ToolTip('The amount of spoolup to use by default on module which support it. Can be changed on a per-module basis'))
spoolup_sizer.Add(self.spool_up_label, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5)
self.spoolup_value = IntCtrl(panel, min=0, max=100, limited=True)
spoolup_sizer.Add(self.spoolup_value , 0, wx.ALL, 5)
mainSizer.Add(spoolup_sizer, 0, wx.ALL | wx.EXPAND, 0)
# Future code once new cap sim is implemented # Future code once new cap sim is implemented
''' '''
self.cbGlobalForceReactivationTimer = wx.CheckBox( panel, wx.ID_ANY, u"Factor in reactivation timer", wx.DefaultPosition, wx.DefaultSize, 0 ) self.cbGlobalForceReactivationTimer = wx.CheckBox( panel, wx.ID_ANY, u"Factor in reactivation timer", wx.DefaultPosition, wx.DefaultSize, 0 )
@@ -96,9 +113,15 @@ class PFFittingEnginePref(PreferenceView):
self.cbUniversalAdaptiveArmorHardener.SetValue(self.engine_settings.get("useStaticAdaptiveArmorHardener")) self.cbUniversalAdaptiveArmorHardener.SetValue(self.engine_settings.get("useStaticAdaptiveArmorHardener"))
self.cbUniversalAdaptiveArmorHardener.Bind(wx.EVT_CHECKBOX, self.OnCBUniversalAdaptiveArmorHardenerChange) self.cbUniversalAdaptiveArmorHardener.Bind(wx.EVT_CHECKBOX, self.OnCBUniversalAdaptiveArmorHardenerChange)
self.spoolup_value.SetValue(int(self.engine_settings.get("globalDefaultSpoolupPercentage") * 100))
self.spoolup_value.Bind(wx.lib.intctrl.EVT_INT, self.OnSpoolupChange)
panel.SetSizer(mainSizer) panel.SetSizer(mainSizer)
panel.Layout() panel.Layout()
def OnSpoolupChange(self, event):
self.engine_settings.set("globalDefaultSpoolupPercentage", self.spoolup_value.GetValue() / 100)
def OnCBGlobalForceReloadStateChange(self, event): def OnCBGlobalForceReloadStateChange(self, event):
self.sFit.serviceFittingOptions["useGlobalForceReload"] = self.cbGlobalForceReload.GetValue() self.sFit.serviceFittingOptions["useGlobalForceReload"] = self.cbGlobalForceReload.GetValue()
fitID = self.mainFrame.getActiveFit() fitID = self.mainFrame.getActiveFit()

View File

@@ -25,7 +25,7 @@ from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount, roundToPrec from gui.utils.numberFormatter import formatAmount, roundToPrec
from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.spoolSupport import SpoolType, SpoolOptions
from service.fit import Fit from service.fit import Fit
import eos.config
class FirepowerViewFull(StatsView): class FirepowerViewFull(StatsView):
name = "firepowerViewFull" name = "firepowerViewFull"
@@ -157,8 +157,7 @@ class FirepowerViewFull(StatsView):
formatAmount(preSpool, prec, lowest, highest), formatAmount(preSpool, prec, lowest, highest),
formatAmount(fullSpool, prec, lowest, highest)) formatAmount(fullSpool, prec, lowest, highest))
# TODO: fetch spoolup option defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
defaultSpoolValue = 1
stats = ( stats = (
( (
"labelFullDpsWeapon", "labelFullDpsWeapon",

View File

@@ -23,6 +23,7 @@ from gui.statsView import StatsView
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount, roundToPrec from gui.utils.numberFormatter import formatAmount, roundToPrec
from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.spoolSupport import SpoolType, SpoolOptions
import eos.config
stats = [ stats = [
@@ -101,8 +102,7 @@ class OutgoingViewFull(StatsView):
formatAmount(preSpool, prec, lowest, highest), formatAmount(preSpool, prec, lowest, highest),
formatAmount(fullSpool, prec, lowest, highest)) formatAmount(fullSpool, prec, lowest, highest))
# TODO: fetch spoolup option defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
defaultSpoolValue = 1
counter = 0 counter = 0
for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats: for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats:
label = getattr(self, labelName) label = getattr(self, labelName)

View File

@@ -22,6 +22,7 @@ import wx
from gui.statsView import StatsView from gui.statsView import StatsView
from gui.utils.numberFormatter import formatAmount, roundToPrec from gui.utils.numberFormatter import formatAmount, roundToPrec
from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.spoolSupport import SpoolType, SpoolOptions
import eos.config
stats = [ stats = [
@@ -100,8 +101,7 @@ class OutgoingViewMinimal(StatsView):
formatAmount(preSpool, prec, lowest, highest), formatAmount(preSpool, prec, lowest, highest),
formatAmount(fullSpool, prec, lowest, highest)) formatAmount(fullSpool, prec, lowest, highest))
# TODO: fetch spoolup option defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
defaultSpoolValue = 1
counter = 0 counter = 0
for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats: for labelName, labelDesc, valueFormat, image, tooltip, val, preSpoolVal, fullSpoolVal, prec, lowest, highest in stats:
label = getattr(self, labelName) label = getattr(self, labelName)

View File

@@ -22,7 +22,7 @@ import wx
from gui.statsView import StatsView from gui.statsView import StatsView
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount from gui.utils.numberFormatter import formatAmount
from service.price import Price from service.price import Fit, Price
from service.settings import PriceMenuSettings from service.settings import PriceMenuSettings
@@ -51,7 +51,7 @@ class PriceViewFull(StatsView):
gridPrice = wx.GridSizer(2, 3, 0, 0) gridPrice = wx.GridSizer(2, 3, 0, 0)
contentSizer.Add(gridPrice, 0, wx.EXPAND | wx.ALL, 0) contentSizer.Add(gridPrice, 0, wx.EXPAND | wx.ALL, 0)
for _type in ("ship", "fittings", "total", "drones", "cargoBay", "character"): for _type in ("ship", "fittings", "character", "drones", "cargoBay", "total"):
if _type in "ship": if _type in "ship":
image = "ship_big" image = "ship_big"
elif _type in ("fittings", "total"): elif _type in ("fittings", "total"):
@@ -79,11 +79,8 @@ class PriceViewFull(StatsView):
def refreshPanel(self, fit): def refreshPanel(self, fit):
if fit is not None: if fit is not None:
self.fit = fit self.fit = fit
fit_items = set(Fit.fitItemIter(fit))
fit_items = Price.fitItemsList(fit) Price.getInstance().getPrices(fit_items, self.processPrices, fetchTimeout=30)
sPrice = Price.getInstance()
sPrice.getPrices(fit_items, self.processPrices)
self.labelEMStatus.SetLabel("Updating prices...") self.labelEMStatus.SetLabel("Updating prices...")
self.refreshPanelPrices(fit) self.refreshPanelPrices(fit)

View File

@@ -22,7 +22,7 @@ import wx
from gui.statsView import StatsView from gui.statsView import StatsView
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount from gui.utils.numberFormatter import formatAmount
from service.price import Price from service.price import Fit, Price
from service.settings import PriceMenuSettings from service.settings import PriceMenuSettings
@@ -73,11 +73,8 @@ class PriceViewMinimal(StatsView):
def refreshPanel(self, fit): def refreshPanel(self, fit):
if fit is not None: if fit is not None:
self.fit = fit self.fit = fit
fit_items = set(Fit.fitItemIter(fit))
fit_items = Price.fitItemsList(fit) Price.getInstance().getPrices(fit_items, self.processPrices, fetchTimeout=30)
sPrice = Price.getInstance()
sPrice.getPrices(fit_items, self.processPrices)
self.labelEMStatus.SetLabel("Updating prices...") self.labelEMStatus.SetLabel("Updating prices...")
self.refreshPanelPrices(fit) self.refreshPanelPrices(fit)

View File

@@ -26,7 +26,7 @@ import gui.mainFrame
from gui.chrome_tabs import EVT_NOTEBOOK_PAGE_CHANGED from gui.chrome_tabs import EVT_NOTEBOOK_PAGE_CHANGED
from gui.utils import fonts from gui.utils import fonts
from eos.saveddata.module import Hardpoint from eos.const import FittingHardpoint
from gui.utils.numberFormatter import formatAmount from gui.utils.numberFormatter import formatAmount
@@ -196,9 +196,9 @@ class ResourcesViewFull(StatsView):
# If we did anything intresting, we'd update our labels to reflect the new fit's stats here # If we did anything intresting, we'd update our labels to reflect the new fit's stats here
stats = ( stats = (
("label%sUsedTurretHardpoints", lambda: fit.getHardpointsUsed(Hardpoint.TURRET), 0, 0, 0), ("label%sUsedTurretHardpoints", lambda: fit.getHardpointsUsed(FittingHardpoint.TURRET), 0, 0, 0),
("label%sTotalTurretHardpoints", lambda: fit.ship.getModifiedItemAttr('turretSlotsLeft'), 0, 0, 0), ("label%sTotalTurretHardpoints", lambda: fit.ship.getModifiedItemAttr('turretSlotsLeft'), 0, 0, 0),
("label%sUsedLauncherHardpoints", lambda: fit.getHardpointsUsed(Hardpoint.MISSILE), 0, 0, 0), ("label%sUsedLauncherHardpoints", lambda: fit.getHardpointsUsed(FittingHardpoint.MISSILE), 0, 0, 0),
("label%sTotalLauncherHardpoints", lambda: fit.ship.getModifiedItemAttr('launcherSlotsLeft'), 0, 0, 0), ("label%sTotalLauncherHardpoints", lambda: fit.ship.getModifiedItemAttr('launcherSlotsLeft'), 0, 0, 0),
("label%sUsedDronesActive", lambda: fit.activeDrones, 0, 0, 0), ("label%sUsedDronesActive", lambda: fit.activeDrones, 0, 0, 0),
("label%sTotalDronesActive", lambda: fit.extraAttributes["maxActiveDrones"], 0, 0, 0), ("label%sTotalDronesActive", lambda: fit.extraAttributes["maxActiveDrones"], 0, 0, 0),
@@ -278,12 +278,16 @@ class ResourcesViewFull(StatsView):
totalCalibrationPoints = value totalCalibrationPoints = value
labelTCP = label labelTCP = label
# See #1877
shown = label.Shown
label.Show(True)
if isinstance(value, str): if isinstance(value, str):
label.SetLabel(value) label.SetLabel(value)
label.SetToolTip(wx.ToolTip(value)) label.SetToolTip(wx.ToolTip(value))
else: else:
label.SetLabel(formatAmount(value, prec, lowest, highest)) label.SetLabel(formatAmount(value, prec, lowest, highest))
label.SetToolTip(wx.ToolTip("%.1f" % value)) label.SetToolTip(wx.ToolTip("%.1f" % value))
label.Show(shown)
colorWarn = wx.Colour(204, 51, 51) colorWarn = wx.Colour(204, 51, 51)
colorNormal = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) colorNormal = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)

View File

@@ -2,8 +2,9 @@
import wx import wx
from eos.saveddata.implant import Implant from eos.saveddata.implant import Implant
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.module import Module, Slot, Rack from eos.saveddata.module import Module, Rack
from eos.saveddata.fit import Fit from eos.saveddata.fit import Fit
from eos.const import FittingSlot
from gui.viewColumn import ViewColumn from gui.viewColumn import ViewColumn
@@ -32,7 +33,7 @@ class BaseIcon(ViewColumn):
return self.shipImage return self.shipImage
elif isinstance(stuff, Module): elif isinstance(stuff, Module):
if stuff.isEmpty: if stuff.isEmpty:
return self.fittingView.imageList.GetImageIndex("slot_%s_small" % Slot.getName(stuff.slot).lower(), return self.fittingView.imageList.GetImageIndex("slot_%s_small" % FittingSlot(stuff.slot).name.lower(),
"gui") "gui")
else: else:
return self.loadIconFile(stuff.item.iconID or "") return self.loadIconFile(stuff.item.iconID or "")

View File

@@ -25,8 +25,9 @@ from eos.saveddata.cargo import Cargo
from eos.saveddata.implant import Implant from eos.saveddata.implant import Implant
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter from eos.saveddata.fighter import Fighter
from eos.saveddata.module import Module, Slot, Rack from eos.saveddata.module import Module, Rack
from eos.saveddata.fit import Fit from eos.saveddata.fit import Fit
from eos.const import FittingSlot
from service.fit import Fit as FitSvc from service.fit import Fit as FitSvc
from service.market import Market from service.market import Market
from gui.viewColumn import ViewColumn from gui.viewColumn import ViewColumn
@@ -72,10 +73,10 @@ class BaseName(ViewColumn):
return "%s (%s)" % (stuff.name, stuff.ship.item.name) return "%s (%s)" % (stuff.name, stuff.ship.item.name)
elif isinstance(stuff, Rack): elif isinstance(stuff, Rack):
if FitSvc.getInstance().serviceFittingOptions["rackLabels"]: if FitSvc.getInstance().serviceFittingOptions["rackLabels"]:
if stuff.slot == Slot.MODE: if stuff.slot == FittingSlot.MODE:
return '─ Tactical Mode ─' return '─ Tactical Mode ─'
else: else:
return '{} {} Slot{}'.format(stuff.num, Slot.getName(stuff.slot).capitalize(), '' if stuff.num == 1 else 's') return '{} {} Slot{}'.format(stuff.num, FittingSlot(stuff.slot).name.capitalize(), '' if stuff.num == 1 else 's')
else: else:
return "" return ""
elif isinstance(stuff, Module): elif isinstance(stuff, Module):
@@ -89,7 +90,7 @@ class BaseName(ViewColumn):
return "{} {}".format(type.name, stuff.item.name[-1:]) return "{} {}".format(type.name, stuff.item.name[-1:])
if stuff.isEmpty: if stuff.isEmpty:
return "%s Slot" % Slot.getName(stuff.slot).capitalize() return "%s Slot" % FittingSlot(stuff.slot).name.capitalize()
else: else:
return stuff.item.name return stuff.item.name
elif isinstance(stuff, Implant): elif isinstance(stuff, Implant):

View File

@@ -28,6 +28,7 @@ from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount from gui.utils.numberFormatter import formatAmount
from gui.utils.listFormatter import formatList from gui.utils.listFormatter import formatList
from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.spoolSupport import SpoolType, SpoolOptions
import eos.config
class Miscellanea(ViewColumn): class Miscellanea(ViewColumn):
@@ -117,8 +118,8 @@ class Miscellanea(ViewColumn):
text = "{0}".format(formatAmount(trackingSpeed, 3, 0, 3)) text = "{0}".format(formatAmount(trackingSpeed, 3, 0, 3))
tooltip = "tracking speed" tooltip = "tracking speed"
info.append((text, tooltip)) info.append((text, tooltip))
# TODO: fetch spoolup option
defaultSpoolValue = 1 defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
spoolTime = stuff.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))[1] spoolTime = stuff.getSpoolData(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))[1]
if spoolTime: if spoolTime:
text = "{0}s".format(formatAmount(spoolTime, 3, 0, 3)) text = "{0}s".format(formatAmount(spoolTime, 3, 0, 3))
@@ -339,8 +340,7 @@ class Miscellanea(ViewColumn):
tooltip = "Armor repaired per second" tooltip = "Armor repaired per second"
return text, tooltip return text, tooltip
elif itemGroup == "Mutadaptive Remote Armor Repairer": elif itemGroup == "Mutadaptive Remote Armor Repairer":
# TODO: fetch spoolup option defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
defaultSpoolValue = 1
spoolOptDefault = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False) spoolOptDefault = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)
spoolOptPre = SpoolOptions(SpoolType.SCALE, 0, True) spoolOptPre = SpoolOptions(SpoolType.SCALE, 0, True)
spoolOptFull = SpoolOptions(SpoolType.SCALE, 1, True) spoolOptFull = SpoolOptions(SpoolType.SCALE, 1, True)

View File

@@ -22,6 +22,7 @@ import wx
from eos.saveddata.cargo import Cargo from eos.saveddata.cargo import Cargo
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
from eos.saveddata.price import PriceStatus from eos.saveddata.price import PriceStatus
from service.price import Price as ServicePrice from service.price import Price as ServicePrice
from gui.viewColumn import ViewColumn from gui.viewColumn import ViewColumn
@@ -29,6 +30,20 @@ from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount from gui.utils.numberFormatter import formatAmount
def formatPrice(stuff, priceObj):
textItems = []
if priceObj.price:
mult = 1
if isinstance(stuff, (Drone, Cargo)):
mult = stuff.amount
elif isinstance(stuff, Fighter):
mult = stuff.amountActive
textItems.append(formatAmount(priceObj.price * mult, 3, 3, 9, currency=True))
if priceObj.status in (PriceStatus.fetchFail, PriceStatus.fetchTimeout):
textItems.append("(!)")
return " ".join(textItems)
class Price(ViewColumn): class Price(ViewColumn):
name = "Price" name = "Price"
@@ -48,35 +63,21 @@ class Price(ViewColumn):
priceObj = stuff.item.price priceObj = stuff.item.price
if not priceObj.isValid: if not priceObj.isValid():
return False return False
# Fetch actual price as float to not modify its value on Price object return formatPrice(stuff, priceObj)
price = priceObj.price
if price == 0:
return ""
if isinstance(stuff, Drone) or isinstance(stuff, Cargo):
price *= stuff.amount
return formatAmount(price, 3, 3, 9, currency=True)
def delayedText(self, mod, display, colItem): def delayedText(self, mod, display, colItem):
sPrice = ServicePrice.getInstance() sPrice = ServicePrice.getInstance()
def callback(item): def callback(item):
price = item[0] priceObj = item[0]
textItems = [] colItem.SetText(formatPrice(mod, priceObj))
if price.price:
textItems.append(formatAmount(price.price, 3, 3, 9, currency=True))
if price.status == PriceStatus.fail:
textItems.append("(!)")
colItem.SetText(" ".join(textItems))
display.SetItem(colItem) display.SetItem(colItem)
sPrice.getPrices([mod.item], callback, True) sPrice.getPrices([mod.item], callback, waitforthread=True)
def getImageId(self, mod): def getImageId(self, mod):
return -1 return -1

View File

@@ -24,7 +24,8 @@ import wx
from eos.saveddata.fit import Fit from eos.saveddata.fit import Fit
from eos.saveddata.implant import Implant from eos.saveddata.implant import Implant
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.module import Module, State as State_, Rack from eos.saveddata.module import Module, Rack
from eos.const import FittingModuleState as State_
from gui.viewColumn import ViewColumn from gui.viewColumn import ViewColumn
import gui.mainFrame import gui.mainFrame
@@ -46,12 +47,11 @@ class State(ViewColumn):
def getToolTip(self, mod): def getToolTip(self, mod):
if isinstance(mod, Module) and not mod.isEmpty: if isinstance(mod, Module) and not mod.isEmpty:
return State_.getName(mod.state).title() return State_(mod.state).name.title()
def getImageId(self, stuff): def getImageId(self, stuff):
generic_active = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(1).lower(), "gui") generic_active = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.ACTIVE.name.lower(), "gui")
generic_inactive = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(-1).lower(), generic_inactive = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.OFFLINE.name.lower(), "gui")
"gui")
if isinstance(stuff, Drone): if isinstance(stuff, Drone):
if stuff.amountActive > 0: if stuff.amountActive > 0:
@@ -64,7 +64,7 @@ class State(ViewColumn):
if stuff.isEmpty: if stuff.isEmpty:
return -1 return -1
else: else:
return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(stuff.state).lower(), return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_(stuff.state).name.lower(),
"gui") "gui")
elif isinstance(stuff, Fit): elif isinstance(stuff, Fit):
fitID = self.mainFrame.getActiveFit() fitID = self.mainFrame.getActiveFit()
@@ -83,7 +83,7 @@ class State(ViewColumn):
return generic_inactive return generic_inactive
elif isinstance(stuff, Implant) and stuff.character: elif isinstance(stuff, Implant) and stuff.character:
# if we're showing character implants, show an "online" state, which should not be changed # if we're showing character implants, show an "online" state, which should not be changed
return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(0).lower(), "gui") return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.ONLINE.name.lower(), "gui")
else: else:
active = getattr(stuff, "active", None) active = getattr(stuff, "active", None)
if active is None: if active is None:

View File

@@ -30,7 +30,8 @@ import gui.globalEvents as GE
import gui.mainFrame import gui.mainFrame
import gui.multiSwitch import gui.multiSwitch
from eos.saveddata.mode import Mode from eos.saveddata.mode import Mode
from eos.saveddata.module import Module, Rack, Slot from eos.saveddata.module import Module, Rack
from eos.const import FittingSlot
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from gui.builtinMarketBrowser.events import ITEM_SELECTED from gui.builtinMarketBrowser.events import ITEM_SELECTED
from gui.builtinShipBrowser.events import EVT_FIT_REMOVED, EVT_FIT_RENAMED, EVT_FIT_SELECTED, FitSelected from gui.builtinShipBrowser.events import EVT_FIT_REMOVED, EVT_FIT_RENAMED, EVT_FIT_SELECTED, FitSelected
@@ -474,7 +475,14 @@ class FittingView(d.Display):
sFit = Fit.getInstance() sFit = Fit.getInstance()
fit = sFit.getFit(self.activeFitID) fit = sFit.getFit(self.activeFitID)
slotOrder = [Slot.SUBSYSTEM, Slot.HIGH, Slot.MED, Slot.LOW, Slot.RIG, Slot.SERVICE] slotOrder = [
FittingSlot.SUBSYSTEM,
FittingSlot.HIGH,
FittingSlot.MED,
FittingSlot.LOW,
FittingSlot.RIG,
FittingSlot.SERVICE
]
if fit is not None: if fit is not None:
self.mods = fit.modules[:] self.mods = fit.modules[:]
@@ -507,7 +515,7 @@ class FittingView(d.Display):
# while also marking the mode header position in the Blanks list # while also marking the mode header position in the Blanks list
if sFit.serviceFittingOptions["rackSlots"]: if sFit.serviceFittingOptions["rackSlots"]:
self.blanks.append(len(self.mods)) self.blanks.append(len(self.mods))
self.mods.append(Rack.buildRack(Slot.MODE, None)) self.mods.append(Rack.buildRack(FittingSlot.MODE, None))
self.mods.append(fit.mode) self.mods.append(fit.mode)
else: else:
@@ -648,8 +656,7 @@ class FittingView(d.Display):
slotMap = {} slotMap = {}
# test for too many modules (happens with t3s / CCP change in slot layout) # test for too many modules (happens with t3s / CCP change in slot layout)
for slotType in Slot.getTypes(): for slot in [e.value for e in FittingSlot]:
slot = Slot.getValue(slotType)
slotMap[slot] = fit.getSlotsFree(slot) < 0 slotMap[slot] = fit.getSlotsFree(slot) < 0
for i, mod in enumerate(self.mods): for i, mod in enumerate(self.mods):
@@ -735,8 +742,8 @@ class FittingView(d.Display):
return return
slotMap = {} slotMap = {}
for slotType in Slot.getTypes():
slot = Slot.getValue(slotType) for slot in [e.value for e in FittingSlot]:
slotMap[slot] = fit.getSlotsFree(slot) < 0 slotMap[slot] = fit.getSlotsFree(slot) < 0
padding = 2 padding = 2

View File

@@ -150,7 +150,7 @@ class CharacterEntityEditor(EntityEditor):
class CharacterEditor(wx.Frame): class CharacterEditor(wx.Frame):
def __init__(self, parent): def __init__(self, parent):
wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="pyfa: Character Editor", pos=wx.DefaultPosition, wx.Frame.__init__(self, parent, id=wx.ID_ANY, title="pyfa: Character Editor", pos=wx.DefaultPosition,
size=wx.Size(640, 600), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER) size=wx.Size(640, 600), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER | wx.FRAME_FLOAT_ON_PARENT)
i = wx.Icon(BitmapLoader.getBitmap("character_small", "gui")) i = wx.Icon(BitmapLoader.getBitmap("character_small", "gui"))
self.SetIcon(i) self.SetIcon(i)
@@ -353,7 +353,7 @@ class SkillTreeView(wx.Panel):
self.skillBookDirtyImageId = self.imageList.Add(wx.Icon(BitmapLoader.getBitmap("skill_small_red", "gui"))) self.skillBookDirtyImageId = self.imageList.Add(wx.Icon(BitmapLoader.getBitmap("skill_small_red", "gui")))
tree.AppendColumn("Skill") tree.AppendColumn("Skill")
tree.AppendColumn("Level") tree.AppendColumn("Level", align=wx.ALIGN_CENTER)
# tree.SetMainColumn(0) # tree.SetMainColumn(0)
self.root = tree.GetRootItem() self.root = tree.GetRootItem()
@@ -361,13 +361,17 @@ class SkillTreeView(wx.Panel):
# #
# tree.SetItemText(self.root, 1, "Levels") # tree.SetItemText(self.root, 1, "Levels")
# tree.SetColumnWidth(0, 300) # first one doesn't work right in Windows. Second one doesn't work right in GTK. Together, we make sure it works.
# Gotta love wx
tree.SetColumnWidth(0, 525)
tree.SetColumnWidth(1, 100)
self.btnSecStatus = wx.Button(self, wx.ID_ANY, "Sec Status: {0:.2f}".format(char.secStatus or 0.0)) self.btnSecStatus = wx.Button(self, wx.ID_ANY, "Sec Status: {0:.2f}".format(char.secStatus or 0.0))
self.btnSecStatus.Bind(wx.EVT_BUTTON, self.onSecStatus) self.btnSecStatus.Bind(wx.EVT_BUTTON, self.onSecStatus)
self.populateSkillTree() self.populateSkillTree()
tree.Bind(wx.dataview.EVT_TREELIST_ITEM_ACTIVATED, self.expand)
tree.Bind(wx.dataview.EVT_TREELIST_ITEM_EXPANDING, self.expandLookup) tree.Bind(wx.dataview.EVT_TREELIST_ITEM_EXPANDING, self.expandLookup)
tree.Bind(wx.dataview.EVT_TREELIST_ITEM_CONTEXT_MENU, self.scheduleMenu) tree.Bind(wx.dataview.EVT_TREELIST_ITEM_CONTEXT_MENU, self.scheduleMenu)
@@ -427,7 +431,8 @@ class SkillTreeView(wx.Panel):
self.levelChangeMenu.Bind(wx.EVT_MENU, self.changeLevel) self.levelChangeMenu.Bind(wx.EVT_MENU, self.changeLevel)
self.SetSizer(pmainSizer) self.SetSizer(pmainSizer)
self.Layout() # This cuases issues with GTK, see #1866
# self.Layout()
def importSkills(self, evt): def importSkills(self, evt):
@@ -554,9 +559,18 @@ class SkillTreeView(wx.Panel):
if event: if event:
event.Skip() event.Skip()
def expand(self, event):
root = event.GetItem()
tree = self.skillTreeListCtrl
if tree.IsExpanded(root):
tree.Collapse(root)
else:
tree.Expand(root)
def expandLookup(self, event): def expandLookup(self, event):
root = event.GetItem() root = event.GetItem()
tree = self.skillTreeListCtrl tree = self.skillTreeListCtrl
child = tree.GetFirstChild(root) child = tree.GetFirstChild(root)
if tree.GetItemText(child) == "dummy": if tree.GetItemText(child) == "dummy":
tree.DeleteItem(child) tree.DeleteItem(child)

View File

@@ -183,6 +183,7 @@ from gui.builtinContextMenus import ( # noqa: E402,F401
openFit, openFit,
moduleGlobalAmmoPicker, moduleGlobalAmmoPicker,
moduleAmmoPicker, moduleAmmoPicker,
spoolUp,
itemStats, itemStats,
damagePattern, damagePattern,
marketJump, marketJump,

View File

@@ -26,6 +26,10 @@ import wx
from service.port.eft import EFT_OPTIONS from service.port.eft import EFT_OPTIONS
from service.port.multibuy import MULTIBUY_OPTIONS from service.port.multibuy import MULTIBUY_OPTIONS
from service.settings import SettingsProvider from service.settings import SettingsProvider
from service.port import EfsPort, Port
from service.const import PortMultiBuyOptions
from eos.db import getFit
from gui.utils.clipboard import toClipboard
class CopySelectDialog(wx.Dialog): class CopySelectDialog(wx.Dialog):
@@ -39,6 +43,17 @@ class CopySelectDialog(wx.Dialog):
def __init__(self, parent): def __init__(self, parent):
wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="Select a format", size=(-1, -1), wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="Select a format", size=(-1, -1),
style=wx.DEFAULT_DIALOG_STYLE) style=wx.DEFAULT_DIALOG_STYLE)
self.CopySelectDict = {
CopySelectDialog.copyFormatEft : self.exportEft,
CopySelectDialog.copyFormatXml : self.exportXml,
CopySelectDialog.copyFormatDna : self.exportDna,
CopySelectDialog.copyFormatEsi : self.exportEsi,
CopySelectDialog.copyFormatMultiBuy: self.exportMultiBuy,
CopySelectDialog.copyFormatEfs : self.exportEfs
}
self.mainFrame = parent
mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer = wx.BoxSizer(wx.VERTICAL)
self.copyFormats = OrderedDict(( self.copyFormats = OrderedDict((
@@ -87,8 +102,8 @@ class CopySelectDialog(wx.Dialog):
self.options[formatId][optId] = checkbox self.options[formatId][optId] = checkbox
if self.settings['options'].get(formatId, {}).get(optId, defaultFormatOptions.get(formatId, {}).get(optId)): if self.settings['options'].get(formatId, {}).get(optId, defaultFormatOptions.get(formatId, {}).get(optId)):
checkbox.SetValue(True) checkbox.SetValue(True)
bsizer.Add(checkbox, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 3) bsizer.Add(checkbox, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 3)
mainSizer.Add(bsizer, 1, wx.EXPAND | wx.LEFT, 20) mainSizer.Add(bsizer, 0, wx.EXPAND | wx.LEFT, 20)
buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL) buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
if buttonSizer: if buttonSizer:
@@ -99,6 +114,31 @@ class CopySelectDialog(wx.Dialog):
self.Fit() self.Fit()
self.Center() self.Center()
def Validate(self):
# Since this dialog is shown through aa ShowModal(), we hook into the Validate function to veto the closing of the dialog until we're ready.
# This always returns False, and when we're ready will EndModal()
selected = self.GetSelected()
options = self.GetOptions()
settings = SettingsProvider.getInstance().getSettings("pyfaExport")
settings["format"] = selected
settings["options"] = options
self.waitDialog = None
def cb(text):
if self.waitDialog:
del self.waitDialog
toClipboard(text)
self.EndModal(wx.ID_OK)
export_options = options.get(selected)
if selected == CopySelectDialog.copyFormatMultiBuy and export_options.get(PortMultiBuyOptions.OPTIMIZE_PRICES, False):
self.waitDialog = wx.BusyInfo("Optimizing Prices", parent=self)
self.CopySelectDict[selected](export_options, callback=cb)
return False
def Selected(self, event): def Selected(self, event):
obj = event.GetEventObject() obj = event.GetEventObject()
formatName = obj.GetLabel() formatName = obj.GetLabel()
@@ -119,3 +159,27 @@ class CopySelectDialog(wx.Dialog):
for formatId in self.options: for formatId in self.options:
options[formatId] = {optId: ch.IsChecked() for optId, ch in self.options[formatId].items()} options[formatId] = {optId: ch.IsChecked() for optId, ch in self.options[formatId].items()}
return options return options
def exportEft(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())
Port.exportEft(fit, options, callback)
def exportDna(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())
Port.exportDna(fit, callback)
def exportEsi(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())
Port.exportESI(fit, callback)
def exportXml(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())
Port.exportXml(None, fit, callback)
def exportMultiBuy(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())
Port.exportMultiBuy(fit, options, callback)
def exportEfs(self, options, callback):
fit = getFit(self.mainFrame.getActiveFit())
EfsPort.exportEfs(fit, 0, callback)

View File

@@ -35,3 +35,5 @@ from .guiToggleDrone import GuiToggleDroneCommand
from .guiFitRename import GuiFitRenameCommand from .guiFitRename import GuiFitRenameCommand
from .guiChangeImplantLocation import GuiChangeImplantLocation from .guiChangeImplantLocation import GuiChangeImplantLocation
from .guiImportMutatedModule import GuiImportMutatedModuleCommand from .guiImportMutatedModule import GuiImportMutatedModuleCommand
from .guiSetSpoolup import GuiSetSpoolup
from .guiRebaseItems import GuiRebaseItemsCommand

View File

@@ -1,5 +1,6 @@
import wx import wx
from eos.saveddata.module import Module, State from eos.saveddata.module import Module
from eos.const import FittingModuleState
import eos.db import eos.db
from logbook import Logger from logbook import Logger
from service.fit import Fit from service.fit import Fit
@@ -52,8 +53,8 @@ class FitAddModuleCommand(wx.Command):
self.module.owner = fit self.module.owner = fit
numSlots = len(fit.modules) numSlots = len(fit.modules)
fit.modules.append(self.module) fit.modules.append(self.module)
if self.module.isValidState(State.ACTIVE): if self.module.isValidState(FittingModuleState.ACTIVE):
self.module.state = State.ACTIVE self.module.state = FittingModuleState.ACTIVE
# todo: fix these # todo: fix these
# As some items may affect state-limiting attributes of the ship, calculate new attributes first # As some items may affect state-limiting attributes of the ship, calculate new attributes first

View File

@@ -1,5 +1,6 @@
import wx import wx
from eos.saveddata.module import Module, State from eos.saveddata.module import Module
from eos.const import FittingModuleState
import eos.db import eos.db
from logbook import Logger from logbook import Logger
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -28,7 +29,7 @@ class FitAddProjectedEnvCommand(wx.Command):
# todo: thing to check for existing environmental effects # todo: thing to check for existing environmental effects
module.state = State.ONLINE module.state = FittingModuleState.ONLINE
if module.isExclusiveSystemEffect: if module.isExclusiveSystemEffect:
# if this is an exclusive system effect, we need to cache the old one. We make room for the new one here, which returns the old one # if this is an exclusive system effect, we need to cache the old one. We make room for the new one here, which returns the old one
self.old_item = fit.projectedModules.makeRoom(module) self.old_item = fit.projectedModules.makeRoom(module)

View File

@@ -1,7 +1,8 @@
import wx import wx
import eos.db import eos.db
from logbook import Logger from logbook import Logger
from eos.saveddata.module import Module, State from eos.saveddata.module import Module
from eos.const import FittingModuleState
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -27,9 +28,9 @@ class FitAddProjectedModuleCommand(wx.Command):
except ValueError: except ValueError:
return False return False
module.state = State.ACTIVE module.state = FittingModuleState.ACTIVE
if not module.canHaveState(module.state, fit): if not module.canHaveState(module.state, fit):
module.state = State.OFFLINE module.state = FittingModuleState.OFFLINE
fit.projectedModules.append(module) fit.projectedModules.append(module)
eos.db.commit() eos.db.commit()

View File

@@ -1,5 +1,6 @@
import wx import wx
from eos.saveddata.module import Module, State from eos.saveddata.module import Module
from eos.const import FittingModuleState
import eos.db import eos.db
from eos.db.gamedata.queries import getDynamicItem from eos.db.gamedata.queries import getDynamicItem
from logbook import Logger from logbook import Logger
@@ -62,8 +63,8 @@ class FitImportMutatedCommand(wx.Command):
module.owner = fit module.owner = fit
numSlots = len(fit.modules) numSlots = len(fit.modules)
fit.modules.append(module) fit.modules.append(module)
if module.isValidState(State.ACTIVE): if module.isValidState(FittingModuleState.ACTIVE):
module.state = State.ACTIVE module.state = FittingModuleState.ACTIVE
# todo: fix these # todo: fix these
# As some items may affect state-limiting attributes of the ship, calculate new attributes first # As some items may affect state-limiting attributes of the ship, calculate new attributes first

View File

@@ -0,0 +1,31 @@
import wx
from logbook import Logger
import eos.db
pyfalog = Logger(__name__)
class FitRebaseItemCommand(wx.Command):
def __init__(self, fitID, containerName, position, newTypeID):
wx.Command.__init__(self, True, "Rebase Item")
self.fitID = fitID
self.containerName = containerName
self.position = position
self.newTypeID = newTypeID
self.oldTypeID = None
def Do(self):
fit = eos.db.getFit(self.fitID)
obj = getattr(fit, self.containerName)[self.position]
self.oldTypeID = getattr(obj.item, "ID", None)
newItem = eos.db.getItem(self.newTypeID)
obj.rebase(newItem)
return True
def Undo(self):
cmd = FitRebaseItemCommand(self.fitID, self.containerName, self.position, self.oldTypeID)
return cmd.Do()

View File

@@ -2,7 +2,8 @@ import wx
from logbook import Logger from logbook import Logger
import eos.db import eos.db
from eos.saveddata.module import Module, State from eos.saveddata.module import Module
from eos.const import FittingModuleState
from gui.fitCommands.helpers import ModuleInfoCache from gui.fitCommands.helpers import ModuleInfoCache
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -72,19 +73,21 @@ class FitReplaceModuleCommand(wx.Command):
# Dummy it out in case the next bit fails # Dummy it out in case the next bit fails
fit.modules.toDummy(self.position) fit.modules.toDummy(self.position)
if self.module.fits(fit): if not self.module.fits(fit):
self.module.owner = fit self.Undo()
fit.modules.toModule(self.position, self.module) return False
if self.module.isValidState(State.ACTIVE):
self.module.state = State.ACTIVE
if self.old_module and self.old_module.charge and self.module.isValidCharge(self.old_module.charge): self.module.owner = fit
self.module.charge = self.old_module.charge fit.modules.toModule(self.position, self.module)
if self.module.isValidState(FittingModuleState.ACTIVE):
self.module.state = FittingModuleState.ACTIVE
# Then, check states of all modules and change where needed. This will recalc if needed if self.old_module and self.old_module.charge and self.module.isValidCharge(self.old_module.charge):
# self.checkStates(fit, m) self.module.charge = self.old_module.charge
# fit.fill() # Then, check states of all modules and change where needed. This will recalc if needed
eos.db.commit() # self.checkStates(fit, m)
return True
return False # fit.fill()
eos.db.commit()
return True

View File

@@ -0,0 +1,37 @@
import wx
import eos.db
from logbook import Logger
from eos.saveddata.booster import Booster
pyfalog = Logger(__name__)
class FitSetSpoolupCommand(wx.Command):
def __init__(self, fitID, position, spoolType, spoolAmount):
wx.Command.__init__(self, True)
self.fitID = fitID
self.position = position
self.spoolType = spoolType
self.spoolAmount = spoolAmount
self.projected = False # todo: get this to work with projected modules? Is that a thing?
self.cache = None
def Do(self):
return self.__set(self.spoolType, self.spoolAmount)
def Undo(self):
if self.cache:
self.__set(*self.cache)
return True
def __set(self, type, amount):
fit = eos.db.getFit(self.fitID)
source = fit.modules if not self.projected else fit.projectedModules
mod = source[self.position]
self.cache = mod.spoolType, mod.spoolAmount
mod.spoolType = type
mod.spoolAmount = amount
eos.db.commit()
return True

View File

@@ -8,7 +8,7 @@ from .calc.fitAddDrone import FitAddDroneCommand
class GuiAddDroneCommand(wx.Command): class GuiAddDroneCommand(wx.Command):
def __init__(self, fitID, itemID): def __init__(self, fitID, itemID):
wx.Command.__init__(self, True, "Cargo Add") wx.Command.__init__(self, True, "Drone Add")
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor() self.internal_history = wx.CommandProcessor()

View File

@@ -8,7 +8,7 @@ from .calc.fitAddFighter import FitAddFighterCommand
class GuiAddFighterCommand(wx.Command): class GuiAddFighterCommand(wx.Command):
def __init__(self, fitID, itemID): def __init__(self, fitID, itemID):
wx.Command.__init__(self, True, "Cargo Add") wx.Command.__init__(self, True, "Fighter Add")
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor() self.internal_history = wx.CommandProcessor()

View File

@@ -3,7 +3,7 @@ from service.fit import Fit
import gui.mainFrame import gui.mainFrame
from gui import globalEvents as GE from gui import globalEvents as GE
from eos.saveddata.fit import ImplantLocation from eos.const import ImplantLocation
from .calc.fitAddImplant import FitAddImplantCommand from .calc.fitAddImplant import FitAddImplantCommand
from .calc.fitChangeImplantLocation import FitChangeImplantLocation from .calc.fitChangeImplantLocation import FitChangeImplantLocation

View File

@@ -21,7 +21,7 @@ class GuiCargoToModuleCommand(wx.Command):
""" """
def __init__(self, fitID, moduleIdx, cargoIdx, copy=False): def __init__(self, fitID, moduleIdx, cargoIdx, copy=False):
wx.Command.__init__(self, True, "Module State Change") wx.Command.__init__(self, True, "Cargo to Module")
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.fitID = fitID self.fitID = fitID

View File

@@ -17,7 +17,7 @@ class GuiFillWithModuleCommand(wx.Command):
set the charge on the underlying module (requires position) set the charge on the underlying module (requires position)
:param position: Optional. The position in fit.modules that we are attempting to set the item to :param position: Optional. The position in fit.modules that we are attempting to set the item to
""" """
wx.Command.__init__(self, True, "Module Add: {}".format(itemID)) wx.Command.__init__(self, True, "Module Fill: {}".format(itemID))
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.fitID = fitID self.fitID = fitID

View File

@@ -14,7 +14,7 @@ pyfalog = Logger(__name__)
class GuiModuleToCargoCommand(wx.Command): class GuiModuleToCargoCommand(wx.Command):
def __init__(self, fitID, moduleIdx, cargoIdx, copy=False): def __init__(self, fitID, moduleIdx, cargoIdx, copy=False):
wx.Command.__init__(self, True, "Module State Change") wx.Command.__init__(self, True, "Module to Cargo")
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.fitID = fitID self.fitID = fitID

View File

@@ -0,0 +1,46 @@
import wx
import eos.db
import gui.mainFrame
from gui import globalEvents as GE
from service.fit import Fit
from .calc.fitRebaseItem import FitRebaseItemCommand
from .calc.fitSetCharge import FitSetChargeCommand
class GuiRebaseItemsCommand(wx.Command):
def __init__(self, fitID, rebaseMap):
wx.Command.__init__(self, True, "Mass Rebase Item")
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.fitID = fitID
self.rebaseMap = rebaseMap
self.internal_history = wx.CommandProcessor()
def Do(self):
fit = eos.db.getFit(self.fitID)
for mod in fit.modules:
if mod.item is not None and mod.item.ID in self.rebaseMap:
self.internal_history.Submit(FitRebaseItemCommand(self.fitID, "modules", mod.modPosition, self.rebaseMap[mod.item.ID]))
if mod.charge is not None and mod.charge.ID in self.rebaseMap:
self.internal_history.Submit(FitSetChargeCommand(self.fitID, [mod.modPosition], self.rebaseMap[mod.charge.ID]))
for containerName in ("drones", "fighters", "implants", "boosters", "cargo"):
container = getattr(fit, containerName)
for obj in container:
if obj.item is not None and obj.item.ID in self.rebaseMap:
self.internal_history.Submit(FitRebaseItemCommand(self.fitID, containerName, container.index(obj), self.rebaseMap[obj.item.ID]))
if self.internal_history.Commands:
eos.db.commit()
Fit.getInstance().recalc(self.fitID)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID))
return True
else:
return False
def Undo(self):
for _ in self.internal_history.Commands:
self.internal_history.Undo()
eos.db.commit()
Fit.getInstance().recalc(self.fitID)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID))
return True

View File

@@ -8,7 +8,7 @@ from .calc.fitRemoveCargo import FitRemoveCargoCommand
class GuiRemoveCargoCommand(wx.Command): class GuiRemoveCargoCommand(wx.Command):
def __init__(self, fitID, itemID): def __init__(self, fitID, itemID):
wx.Command.__init__(self, True, "Module Charge Add") wx.Command.__init__(self, True, "Cargo Remove")
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor() self.internal_history = wx.CommandProcessor()

View File

@@ -8,7 +8,7 @@ from .calc.fitRemoveDrone import FitRemoveDroneCommand
class GuiRemoveDroneCommand(wx.Command): class GuiRemoveDroneCommand(wx.Command):
def __init__(self, fitID, position, amount=1): def __init__(self, fitID, position, amount=1):
wx.Command.__init__(self, True, "Cargo Add") wx.Command.__init__(self, True, "Drone Remove")
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor() self.internal_history = wx.CommandProcessor()

View File

@@ -8,7 +8,7 @@ from .calc.fitRemoveFighter import FitRemoveFighterCommand
class GuiRemoveFighterCommand(wx.Command): class GuiRemoveFighterCommand(wx.Command):
def __init__(self, fitID, position): def __init__(self, fitID, position):
wx.Command.__init__(self, True, "Module Remove") wx.Command.__init__(self, True, "Fighter Remove")
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.fitID = fitID self.fitID = fitID

View File

@@ -27,7 +27,7 @@ class GuiRemoveProjectedCommand(wx.Command):
} }
def __init__(self, fitID, thing): def __init__(self, fitID, thing):
wx.Command.__init__(self, True, "Projected Add") wx.Command.__init__(self, True, "Projected Remove")
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor() self.internal_history = wx.CommandProcessor()

View File

@@ -8,7 +8,7 @@ from .calc.fitSetMode import FitSetModeCommand
class GuiSetModeCommand(wx.Command): class GuiSetModeCommand(wx.Command):
def __init__(self, fitID, mode): def __init__(self, fitID, mode):
wx.Command.__init__(self, True, "Cargo Add") wx.Command.__init__(self, True, "Mode Set")
self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance() self.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor() self.internal_history = wx.CommandProcessor()

View File

@@ -0,0 +1,32 @@
import wx
from service.fit import Fit
import gui.mainFrame
from gui import globalEvents as GE
from .calc.fitSetSpoolup import FitSetSpoolupCommand
class GuiSetSpoolup(wx.Command):
def __init__(self, fitID, module, spoolupType, spoolupAmount):
wx.Command.__init__(self, True, "Booster Add")
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor()
self.fitID = fitID
self.position = module.modPosition
self.spoolType = spoolupType
self.spoolupAmount = spoolupAmount
def Do(self):
if self.internal_history.Submit(FitSetSpoolupCommand(self.fitID, self.position, self.spoolType, self.spoolupAmount)):
self.sFit.recalc(self.fitID)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID))
return True
return False
def Undo(self):
for _ in self.internal_history.Commands:
self.internal_history.Undo()
self.sFit.recalc(self.fitID)
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.fitID))
return True

View File

@@ -37,7 +37,6 @@ import config
import gui.globalEvents as GE import gui.globalEvents as GE
from eos.config import gamedata_date, gamedata_version from eos.config import gamedata_date, gamedata_version
from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues
from eos.db.saveddata.queries import getFit as db_getFit
# import this to access override setting # import this to access override setting
from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.modifiedAttributeDict import ModifiedAttributeDict
from gui import graphFrame from gui import graphFrame
@@ -64,11 +63,12 @@ from gui.setEditor import ImplantSetEditorDlg
from gui.shipBrowser import ShipBrowser from gui.shipBrowser import ShipBrowser
from gui.statsPane import StatsPane from gui.statsPane import StatsPane
from gui.updateDialog import UpdateDialog from gui.updateDialog import UpdateDialog
from gui.utils.clipboard import fromClipboard, toClipboard from gui.utils.clipboard import fromClipboard
from service.character import Character from service.character import Character
from service.esi import Esi from service.esi import Esi
from service.fit import Fit from service.fit import Fit
from service.port import EfsPort, IPortUser, Port from service.port import IPortUser, Port
from service.price import Price
from service.settings import HTMLExportSettings, SettingsProvider from service.settings import HTMLExportSettings, SettingsProvider
from service.update import Update from service.update import Update
import gui.fitCommands as cmd import gui.fitCommands as cmd
@@ -508,6 +508,8 @@ class MainFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.saveCharAs, id=menuBar.saveCharAsId) self.Bind(wx.EVT_MENU, self.saveCharAs, id=menuBar.saveCharAsId)
# Save current character # Save current character
self.Bind(wx.EVT_MENU, self.revertChar, id=menuBar.revertCharId) self.Bind(wx.EVT_MENU, self.revertChar, id=menuBar.revertCharId)
# Optimize fit price
self.Bind(wx.EVT_MENU, self.optimizeFitPrice, id=menuBar.optimizeFitPrice)
# Browse fittings # Browse fittings
self.Bind(wx.EVT_MENU, self.eveFittings, id=menuBar.eveFittingsId) self.Bind(wx.EVT_MENU, self.eveFittings, id=menuBar.eveFittingsId)
@@ -655,6 +657,23 @@ class MainFrame(wx.Frame):
sChr.revertCharacter(charID) sChr.revertCharacter(charID)
wx.PostEvent(self, GE.CharListUpdated()) wx.PostEvent(self, GE.CharListUpdated())
def optimizeFitPrice(self, event):
fitID = self.getActiveFit()
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit:
def updateFitCb(replacementsCheaper):
del self.waitDialog
del self.disablerAll
rebaseMap = {k.ID: v.ID for k, v in replacementsCheaper.items()}
self.command.Submit(cmd.GuiRebaseItemsCommand(fitID, rebaseMap))
fitItems = {i for i in Fit.fitItemIter(fit) if i is not fit.ship.item}
self.disablerAll = wx.WindowDisabler()
self.waitDialog = wx.BusyInfo("Please Wait...", parent=self)
Price.getInstance().findCheaperReplacements(fitItems, updateFitCb, fetchTimeout=10)
def AdditionsTabSelect(self, event): def AdditionsTabSelect(self, event):
selTab = self.additionsSelect.index(event.GetId()) selTab = self.additionsSelect.index(event.GetId())
@@ -688,30 +707,6 @@ class MainFrame(wx.Frame):
else: else:
self.marketBrowser.search.Focus() self.marketBrowser.search.Focus()
def clipboardEft(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportEft(fit, options))
def clipboardDna(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportDna(fit))
def clipboardEsi(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportESI(fit))
def clipboardXml(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportXml(None, fit))
def clipboardMultiBuy(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(Port.exportMultiBuy(fit, options))
def clipboardEfs(self, options):
fit = db_getFit(self.getActiveFit())
toClipboard(EfsPort.exportEfs(fit, 0))
def importFromClipboard(self, event): def importFromClipboard(self, event):
clipboard = fromClipboard() clipboard = fromClipboard()
activeFit = self.getActiveFit() activeFit = self.getActiveFit()
@@ -728,28 +723,8 @@ class MainFrame(wx.Frame):
self._openAfterImport(importData) self._openAfterImport(importData)
def exportToClipboard(self, event): def exportToClipboard(self, event):
CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft, with CopySelectDialog(self) as dlg:
CopySelectDialog.copyFormatXml: self.clipboardXml, dlg.ShowModal()
CopySelectDialog.copyFormatDna: self.clipboardDna,
CopySelectDialog.copyFormatEsi: self.clipboardEsi,
CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy,
CopySelectDialog.copyFormatEfs: self.clipboardEfs}
dlg = CopySelectDialog(self)
btnPressed = dlg.ShowModal()
if btnPressed == wx.ID_OK:
selected = dlg.GetSelected()
options = dlg.GetOptions()
settings = SettingsProvider.getInstance().getSettings("pyfaExport")
settings["format"] = selected
settings["options"] = options
CopySelectDict[selected](options.get(selected))
try:
dlg.Destroy()
except RuntimeError:
pyfalog.error("Tried to destroy an object that doesn't exist in <exportToClipboard>.")
def exportSkillsNeeded(self, event): def exportSkillsNeeded(self, event):
""" Exports skills needed for active fit and active character """ """ Exports skills needed for active fit and active character """

View File

@@ -28,8 +28,6 @@ import gui.globalEvents as GE
from gui.bitmap_loader import BitmapLoader from gui.bitmap_loader import BitmapLoader
from logbook import Logger from logbook import Logger
# from service.crest import Crest
# from service.crest import CrestModes
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
@@ -59,6 +57,7 @@ class MainMenuBar(wx.MenuBar):
self.importDatabaseDefaultsId = wx.NewId() self.importDatabaseDefaultsId = wx.NewId()
self.toggleIgnoreRestrictionID = wx.NewId() self.toggleIgnoreRestrictionID = wx.NewId()
self.devToolsId = wx.NewId() self.devToolsId = wx.NewId()
self.optimizeFitPrice = wx.NewId()
# pheonix: evaluate if this is needed # pheonix: evaluate if this is needed
if 'wxMac' in wx.PlatformInfo and wx.VERSION >= (3, 0): if 'wxMac' in wx.PlatformInfo and wx.VERSION >= (3, 0):
@@ -101,6 +100,9 @@ class MainMenuBar(wx.MenuBar):
editMenu.Append(self.revertCharId, "Revert Character") editMenu.Append(self.revertCharId, "Revert Character")
editMenu.AppendSeparator() editMenu.AppendSeparator()
self.ignoreRestrictionItem = editMenu.Append(self.toggleIgnoreRestrictionID, "Ignore Fitting Restrictions") self.ignoreRestrictionItem = editMenu.Append(self.toggleIgnoreRestrictionID, "Ignore Fitting Restrictions")
editMenu.AppendSeparator()
editMenu.Append(self.optimizeFitPrice, "Optimize Fit Price")
# Character menu # Character menu
windowMenu = wx.Menu() windowMenu = wx.Menu()
@@ -134,8 +136,6 @@ class MainMenuBar(wx.MenuBar):
preferencesItem.SetBitmap(BitmapLoader.getBitmap("preferences_small", "gui")) preferencesItem.SetBitmap(BitmapLoader.getBitmap("preferences_small", "gui"))
windowMenu.Append(preferencesItem) windowMenu.Append(preferencesItem)
# self.sEsi = Crest.getInstance()
# CREST Menu # CREST Menu
esiMMenu = wx.Menu() esiMMenu = wx.Menu()
self.Append(esiMMenu, "EVE &SSO") self.Append(esiMMenu, "EVE &SSO")

View File

@@ -164,12 +164,12 @@ class ShipBrowser(wx.Panel):
self.categoryList = list(sMkt.getShipRoot()) self.categoryList = list(sMkt.getShipRoot())
self.categoryList.sort(key=lambda _ship: _ship.name) self.categoryList.sort(key=lambda _ship: _ship.name)
counts = sFit.countAllFitsGroupedByShip()
# set map & cache of fittings per category # set map & cache of fittings per category
for cat in self.categoryList: for cat in self.categoryList:
itemIDs = [x.ID for x in cat.items] itemIDs = [x.ID for x in sMkt.getItemsByGroup(cat)]
num = sFit.countFitsWithShip(itemIDs) self.categoryFitCache[cat.ID] = sum([count for shipID, count in counts if shipID in itemIDs]) > 0
self.categoryFitCache[cat.ID] = num > 0
for ship in self.categoryList: for ship in self.categoryList:
if self.filterShipsWithNoFits and not self.categoryFitCache[ship.ID]: if self.filterShipsWithNoFits and not self.categoryFitCache[ship.ID]:
continue continue

View File

@@ -2,6 +2,7 @@ import threading
import time import time
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
import wx import wx
from service.const import PortEftOptions
from service.settings import HTMLExportSettings from service.settings import HTMLExportSettings
from service.fit import Fit from service.fit import Fit
from service.port import Port from service.port import Port
@@ -208,8 +209,10 @@ class exportHtmlThread(threading.Thread):
if self.stopRunning: if self.stopRunning:
return return
try: try:
eftFit = Port.exportEft(getFit(fit[0])) eftFit = Port.exportEft(getFit(fit[0]), options={
print(eftFit) PortEftOptions.IMPLANTS: True,
PortEftOptions.MUTATIONS: True,
PortEftOptions.LOADED_CHARGES: True})
HTMLfit = ( HTMLfit = (
' <li data-role="collapsible" data-iconpos="right" data-shadow="false" ' ' <li data-role="collapsible" data-iconpos="right" data-shadow="false" '

View File

@@ -178,32 +178,17 @@ def main(db, json_path):
def fillReplacements(tables): def fillReplacements(tables):
def compareAttrs(attrs1, attrs2, attrHig): def compareAttrs(attrs1, attrs2):
""" # Consider items as different if they have no attrs
Compares received attribute sets. Returns: if len(attrs1) == 0 and len(attrs2) == 0:
- 0 if sets are different return False
- 1 if sets are exactly the same
- 2 if first set is strictly better
- 3 if second set is strictly better
"""
if set(attrs1) != set(attrs2): if set(attrs1) != set(attrs2):
return 0 return False
if all(attrs1[aid] == attrs2[aid] for aid in attrs1): if all(attrs1[aid] == attrs2[aid] for aid in attrs1):
return 1 return True
if all( return False
(attrs1[aid] >= attrs2[aid] and attrHig[aid]) or
(attrs1[aid] <= attrs2[aid] and not attrHig[aid])
for aid in attrs1
):
return 2
if all(
(attrs2[aid] >= attrs1[aid] and attrHig[aid]) or
(attrs2[aid] <= attrs1[aid] and not attrHig[aid])
for aid in attrs1
):
return 3
return 0
print('finding replacements')
skillReqAttribs = { skillReqAttribs = {
182: 277, 182: 277,
183: 278, 183: 278,
@@ -213,10 +198,17 @@ def main(db, json_path):
1290: 1288} 1290: 1288}
skillReqAttribsFlat = set(skillReqAttribs.keys()).union(skillReqAttribs.values()) skillReqAttribsFlat = set(skillReqAttribs.keys()).union(skillReqAttribs.values())
# Get data on type groups # Get data on type groups
# Format: {type ID: group ID}
typesGroups = {} typesGroups = {}
for row in tables['evetypes']: for row in tables['evetypes']:
typesGroups[row['typeID']] = row['groupID'] typesGroups[row['typeID']] = row['groupID']
# Get data on item effects
# Format: {type ID: set(effect, IDs)}
typesEffects = {}
for row in tables['dgmtypeeffects']:
typesEffects.setdefault(row['typeID'], set()).add(row['effectID'])
# Get data on type attributes # Get data on type attributes
# Format: {type ID: {attribute ID: attribute value}}
typesNormalAttribs = {} typesNormalAttribs = {}
typesSkillAttribs = {} typesSkillAttribs = {}
for row in tables['dgmtypeattribs']: for row in tables['dgmtypeattribs']:
@@ -226,15 +218,23 @@ def main(db, json_path):
typeSkillAttribs[row['attributeID']] = row['value'] typeSkillAttribs[row['attributeID']] = row['value']
# Ignore these attributes for comparison purposes # Ignore these attributes for comparison purposes
elif attributeID in ( elif attributeID in (
# We do not need mass as it affects final ship stats only when carried by ship itself
# (and we're not going to replace ships), but it's wildly inconsistent for other items,
# which otherwise would be the same
4, # mass
124, # mainColor
162, # radius
422, # techLevel 422, # techLevel
633, # metaLevel 633, # metaLevel
1692 # metaGroupID 1692, # metaGroupID
1768 # typeColorScheme
): ):
continue continue
else: else:
typeNormalAttribs = typesNormalAttribs.setdefault(row['typeID'], {}) typeNormalAttribs = typesNormalAttribs.setdefault(row['typeID'], {})
typeNormalAttribs[row['attributeID']] = row['value'] typeNormalAttribs[row['attributeID']] = row['value']
# Get data on skill requirements # Get data on skill requirements
# Format: {type ID: {skill type ID: skill level}}
typesSkillReqs = {} typesSkillReqs = {}
for typeID, typeAttribs in typesSkillAttribs.items(): for typeID, typeAttribs in typesSkillAttribs.items():
typeSkillAttribs = typesSkillAttribs.get(typeID, {}) typeSkillAttribs = typesSkillAttribs.get(typeID, {})
@@ -248,46 +248,54 @@ def main(db, json_path):
except (KeyError, ValueError): except (KeyError, ValueError):
continue continue
typeSkillReqs[skillType] = skillLevel typeSkillReqs[skillType] = skillLevel
# Get data on attribute highIsGood flag # Format: {group ID: category ID}
attrHig = {} groupCategories = {}
for row in tables['dgmattribs']: for row in tables['evegroups']:
attrHig[row['attributeID']] = bool(row['highIsGood']) groupCategories[row['groupID']] = row['categoryID']
# As EVE affects various types mostly depending on their group or skill requirements, # As EVE affects various types mostly depending on their group or skill requirements,
# we're going to group various types up this way # we're going to group various types up this way
# Format: {(group ID, frozenset(skillreq, type, IDs), frozenset(type, effect, IDs): [type ID, {attribute ID: attribute value}]}
groupedData = {} groupedData = {}
for row in tables['evetypes']: for row in tables['evetypes']:
typeID = row['typeID'] typeID = row['typeID']
# Ignore items outside of categories we need
if groupCategories[typesGroups[typeID]] not in (
6, # Ship
7, # Module
8, # Charge
18, # Drone
20, # Implant
22, # Deployable
23, # Starbase
32, # Subsystem
35, # Decryptors
65, # Structure
66, # Structure Module
87, # Fighter
):
continue
typeAttribs = typesNormalAttribs.get(typeID, {}) typeAttribs = typesNormalAttribs.get(typeID, {})
# Ignore stuff w/o attributes # Ignore items w/o attributes
if not typeAttribs: if not typeAttribs:
continue continue
# We need only skill types, not levels for keys # We need only skill types, not levels for keys
typeSkillreqs = frozenset(typesSkillReqs.get(typeID, {})) typeSkillreqs = frozenset(typesSkillReqs.get(typeID, {}))
typeGroup = typesGroups[typeID] typeGroup = typesGroups[typeID]
groupData = groupedData.setdefault((typeGroup, typeSkillreqs), []) typeEffects = frozenset(typesEffects.get(typeID, ()))
groupData = groupedData.setdefault((typeGroup, typeSkillreqs, typeEffects), [])
groupData.append((typeID, typeAttribs)) groupData.append((typeID, typeAttribs))
same = {} # Format: {type ID: set(type IDs)}
better = {} replacements = {}
# Now, go through composed groups and for every item within it find items which are # Now, go through composed groups and for every item within it
# the same and which are better # find items which are the same
for groupData in groupedData.values(): for groupData in groupedData.values():
for type1, type2 in itertools.combinations(groupData, 2): for type1, type2 in itertools.combinations(groupData, 2):
comparisonResult = compareAttrs(type1[1], type2[1], attrHig) if compareAttrs(type1[1], type2[1]):
# Equal replacements.setdefault(type1[0], set()).add(type2[0])
if comparisonResult == 1: replacements.setdefault(type2[0], set()).add(type1[0])
same.setdefault(type1[0], set()).add(type2[0])
same.setdefault(type2[0], set()).add(type1[0])
# First is better
elif comparisonResult == 2:
better.setdefault(type2[0], set()).add(type1[0])
# Second is better
elif comparisonResult == 3:
better.setdefault(type1[0], set()).add(type2[0])
# Put this data into types table so that normal process hooks it up # Put this data into types table so that normal process hooks it up
for row in tables['evetypes']: for row in tables['evetypes']:
typeID = row['typeID'] row['replacements'] = ','.join('{}'.format(tid) for tid in sorted(replacements.get(row['typeID'], ())))
row['replaceSame'] = ','.join('{}'.format(tid) for tid in sorted(same.get(typeID, ())))
row['replaceBetter'] = ','.join('{}'.format(tid) for tid in sorted(better.get(typeID, ())))
data = {} data = {}

View File

@@ -37,7 +37,8 @@ from service.esi import Esi
from eos.saveddata.implant import Implant as es_Implant from eos.saveddata.implant import Implant as es_Implant
from eos.saveddata.character import Character as es_Character, Skill from eos.saveddata.character import Character as es_Character, Skill
from eos.saveddata.module import Slot as es_Slot, Module as es_Module from eos.saveddata.module import Module as es_Module
from eos.const import FittingSlot as es_Slot
from eos.saveddata.fighter import Fighter as es_Fighter from eos.saveddata.fighter import Fighter as es_Fighter
pyfalog = Logger(__name__) pyfalog = Logger(__name__)

104
service/const.py Normal file
View File

@@ -0,0 +1,104 @@
# =============================================================================
# Copyright (C) 2019 Ryan Holmes
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
from enum import Enum, IntEnum, unique, auto
@unique
class EsiLoginMethod(IntEnum):
"""
Contains the method of ESI login
"""
SERVER = 0
MANUAL = 1
@unique
class EsiSsoMode(IntEnum):
"""
Contains the mode of ESI sso mode
"""
AUTO = 0
CUSTOM = 1
class EsiEndpoints(Enum):
"""
Contains the endpoint paths for the ESI access
"""
CHAR = "/v4/characters/{character_id}/"
CHAR_SKILLS = "/v4/characters/{character_id}/skills/"
CHAR_FITTINGS = "/v1/characters/{character_id}/fittings/"
CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/"
@unique
class PortMultiBuyOptions(IntEnum):
"""
Contains different types of items for multibuy export
"""
IMPLANTS = 1
CARGO = 2
LOADED_CHARGES = 3
OPTIMIZE_PRICES = 4
@unique
class PortEftOptions(IntEnum):
"""
Contains different options for eft-export
"""
IMPLANTS = 1
MUTATIONS = 2
LOADED_CHARGES = 3
@unique
class PortEftRigSize(IntEnum):
"""
Contains different sizes of ship rigs
This enum is not actively used, but maybe useful someday.
"""
SMALL = 1
MEDIUM = 2
LARGE = 3
CAPITAL = 4
@unique
class GuiAttrGroup(IntEnum):
"""
Define the various groups of attributes.
This enum is used for GUI functions and getting redefined in
/gui/builtinItemStatsViews/attributeGrouping.py
"""
FITTING = auto()
STRUCTURE = auto()
SHIELD = auto()
ARMOR = auto()
TARGETING = auto()
EWAR_RESISTS = auto()
CAPACITOR = auto()
SHARED_FACILITIES = auto()
FIGHTER_FACILITIES = auto()
ON_DEATH = auto()
JUMP_SYSTEMS = auto()
PROPULSIONS = auto()
FIGHTERS = auto()
SHIP_GROUP = auto()

View File

@@ -9,9 +9,9 @@ import config
import webbrowser import webbrowser
import eos.db import eos.db
from eos.enum import Enum from service.const import EsiLoginMethod, EsiSsoMode
from eos.saveddata.ssocharacter import SsoCharacter from eos.saveddata.ssocharacter import SsoCharacter
from service.esiAccess import APIException, SsoMode from service.esiAccess import APIException
import gui.globalEvents as GE import gui.globalEvents as GE
from gui.ssoLogin import SsoLogin, SsoLoginServer from gui.ssoLogin import SsoLogin, SsoLoginServer
from service.server import StoppableHTTPServer, AuthHandler from service.server import StoppableHTTPServer, AuthHandler
@@ -24,11 +24,6 @@ from requests import Session
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
class LoginMethod(Enum):
SERVER = 0
MANUAL = 1
class Esi(EsiAccess): class Esi(EsiAccess):
_instance = None _instance = None
@@ -107,8 +102,8 @@ class Esi(EsiAccess):
def login(self): def login(self):
# always start the local server if user is using client details. Otherwise, start only if they choose to do so. # always start the local server if user is using client details. Otherwise, start only if they choose to do so.
if self.settings.get('ssoMode') == SsoMode.CUSTOM or self.settings.get('loginMode') == LoginMethod.SERVER: if self.settings.get('ssoMode') == EsiSsoMode.CUSTOM or self.settings.get('loginMode') == EsiLoginMethod.SERVER:
dlg = gui.ssoLogin.SsoLoginServer(6461 if self.settings.get('ssoMode') == SsoMode.CUSTOM else 0) dlg = gui.ssoLogin.SsoLoginServer(6461 if self.settings.get('ssoMode') == EsiSsoMode.CUSTOM else 0)
dlg.ShowModal() dlg.ShowModal()
else: else:
dlg = gui.ssoLogin.SsoLogin() dlg = gui.ssoLogin.SsoLogin()
@@ -142,7 +137,7 @@ class Esi(EsiAccess):
def handleLogin(self, message): def handleLogin(self, message):
# we already have authenticated stuff for the auto mode # we already have authenticated stuff for the auto mode
if self.settings.get('ssoMode') == SsoMode.AUTO: if self.settings.get('ssoMode') == EsiSsoMode.AUTO:
ssoInfo = message['SSOInfo'][0] ssoInfo = message['SSOInfo'][0]
auth_response = json.loads(base64.b64decode(ssoInfo)) auth_response = json.loads(base64.b64decode(ssoInfo))
else: else:

View File

@@ -17,7 +17,7 @@ import config
import base64 import base64
import datetime import datetime
from eos.enum import Enum from service.const import EsiSsoMode, EsiEndpoints
from service.settings import EsiSettings, NetworkSettings from service.settings import EsiSettings, NetworkSettings
from requests import Session from requests import Session
@@ -42,11 +42,6 @@ scopes = [
] ]
class SsoMode(Enum):
AUTO = 0
CUSTOM = 1
class APIException(Exception): class APIException(Exception):
""" Exception for SSO related errors """ """ Exception for SSO related errors """
@@ -66,13 +61,6 @@ class APIException(Exception):
return 'HTTP Error %s' % self.status_code return 'HTTP Error %s' % self.status_code
class ESIEndpoints(Enum):
CHAR = "/v4/characters/{character_id}/"
CHAR_SKILLS = "/v4/characters/{character_id}/skills/"
CHAR_FITTINGS = "/v1/characters/{character_id}/fittings/"
CHAR_DEL_FIT = "/v1/characters/{character_id}/fittings/{fitting_id}/"
class EsiAccess(object): class EsiAccess(object):
def __init__(self): def __init__(self):
self.settings = EsiSettings.getInstance() self.settings = EsiSettings.getInstance()
@@ -89,7 +77,7 @@ class EsiAccess(object):
@property @property
def sso_url(self): def sso_url(self):
if self.settings.get("ssoMode") == SsoMode.CUSTOM: if self.settings.get("ssoMode") == EsiSsoMode.CUSTOM:
return "https://login.eveonline.com" return "https://login.eveonline.com"
return "https://www.pyfa.io" return "https://www.pyfa.io"
@@ -110,20 +98,20 @@ class EsiAccess(object):
return '%s/oauth/token' % self.sso_url return '%s/oauth/token' % self.sso_url
def getSkills(self, char): def getSkills(self, char):
return self.get(char, ESIEndpoints.CHAR_SKILLS, character_id=char.characterID) return self.get(char, EsiEndpoints.CHAR_SKILLS.value, character_id=char.characterID)
def getSecStatus(self, char): def getSecStatus(self, char):
return self.get(char, ESIEndpoints.CHAR, character_id=char.characterID) return self.get(char, EsiEndpoints.CHAR.value, character_id=char.characterID)
def getFittings(self, char): def getFittings(self, char):
return self.get(char, ESIEndpoints.CHAR_FITTINGS, character_id=char.characterID) return self.get(char, EsiEndpoints.CHAR_FITTINGS.value, character_id=char.characterID)
def postFitting(self, char, json_str): def postFitting(self, char, json_str):
# @todo: new fitting ID can be recovered from resp.data, # @todo: new fitting ID can be recovered from resp.data,
return self.post(char, ESIEndpoints.CHAR_FITTINGS, json_str, character_id=char.characterID) return self.post(char, EsiEndpoints.CHAR_FITTINGS.value, json_str, character_id=char.characterID)
def delFitting(self, char, fittingID): def delFitting(self, char, fittingID):
return self.delete(char, ESIEndpoints.CHAR_DEL_FIT, character_id=char.characterID, fitting_id=fittingID) return self.delete(char, EsiEndpoints.CHAR_DEL_FIT.value, character_id=char.characterID, fitting_id=fittingID)
@staticmethod @staticmethod
def update_token(char, tokenResponse): def update_token(char, tokenResponse):
@@ -136,7 +124,7 @@ class EsiAccess(object):
def getLoginURI(self, redirect=None): def getLoginURI(self, redirect=None):
self.state = str(uuid.uuid4()) self.state = str(uuid.uuid4())
if self.settings.get("ssoMode") == SsoMode.AUTO: if self.settings.get("ssoMode") == EsiSsoMode.AUTO:
args = { args = {
'state': self.state, 'state': self.state,
'pyfa_version': config.version, 'pyfa_version': config.version,
@@ -183,7 +171,7 @@ class EsiAccess(object):
'refresh_token': refreshToken, 'refresh_token': refreshToken,
} }
if self.settings.get('ssoMode') == SsoMode.AUTO: if self.settings.get('ssoMode') == EsiSsoMode.AUTO:
# data is all we really need, the rest is handled automatically by pyfa.io # data is all we really need, the rest is handled automatically by pyfa.io
return { return {
'data': data, 'data': data,

View File

@@ -30,8 +30,9 @@ from eos.saveddata.citadel import Citadel as es_Citadel
from eos.saveddata.damagePattern import DamagePattern as es_DamagePattern from eos.saveddata.damagePattern import DamagePattern as es_DamagePattern
from eos.saveddata.drone import Drone as es_Drone from eos.saveddata.drone import Drone as es_Drone
from eos.saveddata.fighter import Fighter as es_Fighter from eos.saveddata.fighter import Fighter as es_Fighter
from eos.saveddata.fit import Fit as FitType, ImplantLocation from eos.const import ImplantLocation, FittingModuleState
from eos.saveddata.module import Module as es_Module, State from eos.saveddata.fit import Fit as FitType
from eos.saveddata.module import Module as es_Module
from eos.saveddata.ship import Ship as es_Ship from eos.saveddata.ship import Ship as es_Ship
from service.character import Character from service.character import Character
from service.damagePattern import DamagePattern from service.damagePattern import DamagePattern
@@ -146,6 +147,11 @@ class Fit(FitDeprecated):
pyfalog.debug("Getting count of all fits.") pyfalog.debug("Getting count of all fits.")
return eos.db.countAllFits() return eos.db.countAllFits()
@staticmethod
def countAllFitsGroupedByShip():
count = eos.db.countFitGroupedByShip()
return count
@staticmethod @staticmethod
def countFitsWithShip(stuff): def countFitsWithShip(stuff):
pyfalog.debug("Getting count of all fits for: {0}", stuff) pyfalog.debug("Getting count of all fits for: {0}", stuff)
@@ -347,7 +353,7 @@ class Fit(FitDeprecated):
elif isinstance(thing, es_Module): elif isinstance(thing, es_Module):
thing.state = es_Module.getProposedState(thing, click) thing.state = es_Module.getProposedState(thing, click)
if not thing.canHaveState(thing.state, fit): if not thing.canHaveState(thing.state, fit):
thing.state = State.OFFLINE thing.state = FittingModuleState.OFFLINE
elif isinstance(thing, FitType): elif isinstance(thing, FitType):
projectionInfo = thing.getProjectionInfo(fitID) projectionInfo = thing.getProjectionInfo(fitID)
if projectionInfo: if projectionInfo:
@@ -379,8 +385,8 @@ class Fit(FitDeprecated):
if m.fits(fit): if m.fits(fit):
m.owner = fit m.owner = fit
fit.modules.toModule(position, m) fit.modules.toModule(position, m)
if m.isValidState(State.ACTIVE): if m.isValidState(FittingModuleState.ACTIVE):
m.state = State.ACTIVE m.state = FittingModuleState.ACTIVE
# As some items may affect state-limiting attributes of the ship, calculate new attributes first # As some items may affect state-limiting attributes of the ship, calculate new attributes first
self.recalc(fit) self.recalc(fit)
@@ -534,13 +540,13 @@ class Fit(FitDeprecated):
if mod != base: if mod != base:
# fix for #529, where a module may be in incorrect state after CCP changes mechanics of module # fix for #529, where a module may be in incorrect state after CCP changes mechanics of module
if not mod.canHaveState(mod.state) or not mod.isValidState(mod.state): if not mod.canHaveState(mod.state) or not mod.isValidState(mod.state):
mod.state = State.ONLINE mod.state = FittingModuleState.ONLINE
changed = True changed = True
for mod in fit.projectedModules: for mod in fit.projectedModules:
# fix for #529, where a module may be in incorrect state after CCP changes mechanics of module # fix for #529, where a module may be in incorrect state after CCP changes mechanics of module
if not mod.canHaveState(mod.state, fit) or not mod.isValidState(mod.state): if not mod.canHaveState(mod.state, fit) or not mod.isValidState(mod.state):
mod.state = State.OFFLINE mod.state = FittingModuleState.OFFLINE
changed = True changed = True
for drone in fit.projectedDrones: for drone in fit.projectedDrones:
@@ -549,9 +555,26 @@ class Fit(FitDeprecated):
changed = True changed = True
return changed return changed
# If any state was changed, recalculate attributes again
# if changed: @classmethod
# self.recalc(fit) def fitObjectIter(cls, fit):
yield fit.ship
for mod in fit.modules:
if not mod.isEmpty:
yield mod
for container in (fit.drones, fit.fighters, fit.implants, fit.boosters, fit.cargo):
for obj in container:
yield obj
@classmethod
def fitItemIter(cls, fit):
for fitobj in cls.fitObjectIter(fit):
yield fitobj.item
charge = getattr(fitobj, 'charge', None)
if charge:
yield charge
def refreshFit(self, fitID): def refreshFit(self, fitID):
pyfalog.debug("Refresh fit for fit ID: {0}", fitID) pyfalog.debug("Refresh fit for fit ID: {0}", fitID)

View File

@@ -27,7 +27,8 @@ from eos.saveddata.cargo import Cargo as es_Cargo
from eos.saveddata.drone import Drone as es_Drone from eos.saveddata.drone import Drone as es_Drone
from eos.saveddata.fighter import Fighter as es_Fighter from eos.saveddata.fighter import Fighter as es_Fighter
from eos.saveddata.implant import Implant as es_Implant from eos.saveddata.implant import Implant as es_Implant
from eos.saveddata.module import Module as es_Module, State from eos.saveddata.module import Module as es_Module
from eos.const import FittingModuleState
from eos.saveddata.fit import Fit as FitType from eos.saveddata.fit import Fit as FitType
from utils.deprecated import deprecated from utils.deprecated import deprecated
@@ -304,16 +305,16 @@ class FitDeprecated(object):
fit.projectedFighters.append(fighter) fit.projectedFighters.append(fighter)
elif thing.group.name in es_Module.SYSTEM_GROUPS: elif thing.group.name in es_Module.SYSTEM_GROUPS:
module = es_Module(thing) module = es_Module(thing)
module.state = State.ONLINE module.state = FittingModuleState.ONLINE
fit.projectedModules.append(module) fit.projectedModules.append(module)
else: else:
try: try:
module = es_Module(thing) module = es_Module(thing)
except ValueError: except ValueError:
return False return False
module.state = State.ACTIVE module.state = FittingModuleState.ACTIVE
if not module.canHaveState(module.state, fit): if not module.canHaveState(module.state, fit):
module.state = State.OFFLINE module.state = FittingModuleState.OFFLINE
fit.projectedModules.append(module) fit.projectedModules.append(module)
eos.db.commit() eos.db.commit()
@@ -396,8 +397,8 @@ class FitDeprecated(object):
m.owner = fit m.owner = fit
numSlots = len(fit.modules) numSlots = len(fit.modules)
fit.modules.append(m) fit.modules.append(m)
if m.isValidState(State.ACTIVE): if m.isValidState(FittingModuleState.ACTIVE):
m.state = State.ACTIVE m.state = FittingModuleState.ACTIVE
# As some items may affect state-limiting attributes of the ship, calculate new attributes first # As some items may affect state-limiting attributes of the ship, calculate new attributes first
self.recalc(fit) self.recalc(fit)
@@ -465,8 +466,8 @@ class FitDeprecated(object):
if m.fits(fit): if m.fits(fit):
m.owner = fit m.owner = fit
fit.modules.toModule(position, m) fit.modules.toModule(position, m)
if m.isValidState(State.ACTIVE): if m.isValidState(FittingModuleState.ACTIVE):
m.state = State.ACTIVE m.state = FittingModuleState.ACTIVE
if recalc: if recalc:
# As some items may affect state-limiting attributes of the ship, calculate new attributes first # As some items may affect state-limiting attributes of the ship, calculate new attributes first
@@ -508,8 +509,8 @@ class FitDeprecated(object):
try: try:
cargoP = es_Module(cargo.item) cargoP = es_Module(cargo.item)
cargoP.owner = fit cargoP.owner = fit
if cargoP.isValidState(State.ACTIVE): if cargoP.isValidState(FittingModuleState.ACTIVE):
cargoP.state = State.ACTIVE cargoP.state = FittingModuleState.ACTIVE
except: except:
pyfalog.warning("Invalid item: {0}", cargo.item) pyfalog.warning("Invalid item: {0}", cargo.item)
return return

View File

@@ -795,3 +795,20 @@ class Market(object):
"""Filter items by meta lvl""" """Filter items by meta lvl"""
filtered = set([item for item in items if self.getMetaGroupIdByItem(item) in metas]) filtered = set([item for item in items if self.getMetaGroupIdByItem(item) in metas])
return filtered return filtered
def getReplacements(self, identity):
item = self.getItem(identity)
# We already store needed type IDs in database
replTypeIDs = {int(i) for i in item.replacements.split(",") if i}
if not replTypeIDs:
return ()
# As replacements were generated without keeping track which items were published,
# filter them out here
items = []
for typeID in replTypeIDs:
item = self.getItem(typeID)
if not item:
continue
if self.getPublicityByItem(item):
items.append(item)
return items

View File

@@ -17,30 +17,37 @@
# along with pyfa. If not, see <http://www.gnu.org/licenses/>. # along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# ============================================================================= # =============================================================================
import time
from xml.dom import minidom from xml.dom import minidom
from logbook import Logger from logbook import Logger
from eos.saveddata.price import PriceStatus from eos.saveddata.price import PriceStatus
from service.network import Network from service.network import Network
from service.price import Price, TIMEOUT, VALIDITY from service.price import Price
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
class EveMarketData(object): class EveMarketData:
name = "eve-marketdata.com" name = "eve-marketdata.com"
def __init__(self, types, system, priceMap): def __init__(self, priceMap, system, fetchTimeout):
data = {} # Try selected system first
baseurl = "https://eve-marketdata.com/api/item_prices.xml" self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system)
data["system_id"] = system # Use Jita for market # If price was not available - try globally
data["type_ids"] = ','.join(str(x) for x in types) if priceMap:
self.fetchPrices(priceMap, max(fetchTimeout / 3, 2))
@staticmethod
def fetchPrices(priceMap, fetchTimeout, system=None):
params = {"type_ids": ','.join(str(typeID) for typeID in priceMap)}
if system is not None:
params["system_id"] = system
baseurl = "https://eve-marketdata.com/api/item_prices.xml"
network = Network.getInstance() network = Network.getInstance()
data = network.request(baseurl, network.PRICES, params=data) data = network.request(baseurl, network.PRICES, params=params, timeout=fetchTimeout)
xml = minidom.parseString(data.text) xml = minidom.parseString(data.text)
types = xml.getElementsByTagName("eve").item(0).getElementsByTagName("price") types = xml.getElementsByTagName("eve").item(0).getElementsByTagName("price")
@@ -55,19 +62,10 @@ class EveMarketData(object):
pyfalog.warning("Failed to get price for: {0}", type_) pyfalog.warning("Failed to get price for: {0}", type_)
continue continue
# Fill price data # eve-marketdata returns 0 if price data doesn't even exist for the item
priceobj = priceMap[typeID] if price == 0:
continue
# eve-marketdata returns 0 if price data doesn't even exist for the item. In this case, don't reset the priceMap[typeID].update(PriceStatus.fetchSuccess, price)
# cached price, and set the price timeout to TIMEOUT (every 15 minutes currently). Se GH issue #1334
if price != 0:
priceobj.price = price
priceobj.time = time.time() + VALIDITY
priceobj.status = PriceStatus.success
else:
priceobj.time = time.time() + TIMEOUT
# delete price from working dict
del priceMap[typeID] del priceMap[typeID]

View File

@@ -17,33 +17,37 @@
# along with pyfa. If not, see <http://www.gnu.org/licenses/>. # along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# ============================================================================= # =============================================================================
import time
from xml.dom import minidom from xml.dom import minidom
from logbook import Logger from logbook import Logger
from eos.saveddata.price import PriceStatus from eos.saveddata.price import PriceStatus
from service.network import Network from service.network import Network
from service.price import Price, VALIDITY from service.price import Price
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
class EveMarketer(object): class EveMarketer:
name = "evemarketer" name = "evemarketer"
def __init__(self, types, system, priceMap): def __init__(self, priceMap, system, fetchTimeout):
data = {} # Try selected system first
self.fetchPrices(priceMap, max(2 * fetchTimeout / 3, 2), system)
# If price was not available - try globally
if priceMap:
self.fetchPrices(priceMap, max(fetchTimeout / 3, 2))
@staticmethod
def fetchPrices(priceMap, fetchTimeout, system=None):
params = {"typeid": {typeID for typeID in priceMap}}
if system is not None:
params["usesystem"] = system
baseurl = "https://api.evemarketer.com/ec/marketstat" baseurl = "https://api.evemarketer.com/ec/marketstat"
data["usesystem"] = system # Use Jita for market
data["typeid"] = set()
for typeID in types: # Add all typeID arguments
data["typeid"].add(typeID)
network = Network.getInstance() network = Network.getInstance()
data = network.request(baseurl, network.PRICES, params=data) data = network.request(baseurl, network.PRICES, params=params, timeout=fetchTimeout)
xml = minidom.parseString(data.text) xml = minidom.parseString(data.text)
types = xml.getElementsByTagName("marketstat").item(0).getElementsByTagName("type") types = xml.getElementsByTagName("marketstat").item(0).getElementsByTagName("type")
# Cycle through all types we've got from request # Cycle through all types we've got from request
@@ -56,15 +60,15 @@ class EveMarketer(object):
percprice = float(sell.getElementsByTagName("percentile").item(0).firstChild.data) percprice = float(sell.getElementsByTagName("percentile").item(0).firstChild.data)
except (TypeError, ValueError): except (TypeError, ValueError):
pyfalog.warning("Failed to get price for: {0}", type_) pyfalog.warning("Failed to get price for: {0}", type_)
percprice = 0 continue
# Fill price data # Price is 0 if evemarketer has info on this item, but it is not available
priceobj = priceMap[typeID] # for current scope limit. If we provided scope limit - make sure to skip
priceobj.price = percprice # such items to check globally, and do not skip if requested globally
priceobj.time = time.time() + VALIDITY if percprice == 0 and system is not None:
priceobj.status = PriceStatus.success continue
# delete price from working dict priceMap[typeID].update(PriceStatus.fetchSuccess, percprice)
del priceMap[typeID] del priceMap[typeID]

View File

@@ -53,7 +53,7 @@ class TimeoutError(Exception):
pass pass
class Network(object): class Network:
# Request constants - every request must supply this, as it is checked if # Request constants - every request must supply this, as it is checked if
# enabled or not via settings # enabled or not via settings
ENABLED = 1 ENABLED = 1

View File

@@ -28,8 +28,9 @@ from eos.saveddata.citadel import Citadel
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter from eos.saveddata.fighter import Fighter
from eos.saveddata.fit import Fit from eos.saveddata.fit import Fit
from eos.saveddata.module import Module, State, Slot from eos.saveddata.module import Module
from eos.saveddata.ship import Ship from eos.saveddata.ship import Ship
from eos.const import FittingSlot, FittingModuleState
from service.fit import Fit as svcFit from service.fit import Fit as svcFit
from service.market import Market from service.market import Market
@@ -106,8 +107,8 @@ def importDna(string):
f.modules.append(m) f.modules.append(m)
else: else:
m.owner = f m.owner = f
if m.isValidState(State.ACTIVE): if m.isValidState(FittingModuleState.ACTIVE):
m.state = State.ACTIVE m.state = FittingModuleState.ACTIVE
moduleList.append(m) moduleList.append(m)
# Recalc to get slot numbers correct for T3 cruisers # Recalc to get slot numbers correct for T3 cruisers
@@ -116,14 +117,14 @@ def importDna(string):
for module in moduleList: for module in moduleList:
if module.fits(f): if module.fits(f):
module.owner = f module.owner = f
if module.isValidState(State.ACTIVE): if module.isValidState(FittingModuleState.ACTIVE):
module.state = State.ACTIVE module.state = FittingModuleState.ACTIVE
f.modules.append(module) f.modules.append(module)
return f return f
def exportDna(fit): def exportDna(fit, callback):
dna = str(fit.shipID) dna = str(fit.shipID)
subsystems = [] # EVE cares which order you put these in subsystems = [] # EVE cares which order you put these in
mods = OrderedDict() mods = OrderedDict()
@@ -131,7 +132,7 @@ def exportDna(fit):
sFit = svcFit.getInstance() sFit = svcFit.getInstance()
for mod in fit.modules: for mod in fit.modules:
if not mod.isEmpty: if not mod.isEmpty:
if mod.slot == Slot.SUBSYSTEM: if mod.slot == FittingSlot.SUBSYSTEM:
subsystems.append(mod) subsystems.append(mod)
continue continue
if mod.itemID not in mods: if mod.itemID not in mods:
@@ -173,4 +174,9 @@ def exportDna(fit):
for charge in charges: for charge in charges:
dna += ":{0};{1}".format(charge, charges[charge]) dna += ":{0};{1}".format(charge, charges[charge])
return dna + "::" text = dna + "::"
if callback:
callback(text)
else:
return text

View File

@@ -6,8 +6,9 @@ from numbers import Number
from config import version as pyfaVersion from config import version as pyfaVersion
from service.fit import Fit from service.fit import Fit
from service.market import Market from service.market import Market
from eos.enum import Enum from eos.const import FittingModuleState, FittingHardpoint, FittingSlot
from eos.saveddata.module import Hardpoint, Slot, Module, State from service.const import PortEftRigSize
from eos.saveddata.module import Module
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.effectHandlerHelpers import HandledList from eos.effectHandlerHelpers import HandledList
from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup
@@ -19,14 +20,6 @@ from logbook import Logger
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
class RigSize(Enum):
# Matches to item attribute "rigSize" on ship and rig items
SMALL = 1
MEDIUM = 2
LARGE = 3
CAPITAL = 4
class EfsPort: class EfsPort:
wepTestSet = {} wepTestSet = {}
version = 0.03 version = 0.03
@@ -58,13 +51,13 @@ class EfsPort:
mwd50mn = mapPropData("50MN Microwarpdrive II") mwd50mn = mapPropData("50MN Microwarpdrive II")
mwd500mn = mapPropData("500MN Microwarpdrive II") mwd500mn = mapPropData("500MN Microwarpdrive II")
mwd50000mn = mapPropData("50000MN Microwarpdrive II") mwd50000mn = mapPropData("50000MN Microwarpdrive II")
if rigSize == RigSize.SMALL or rigSize is None: if rigSize == PortEftRigSize.SMALL or rigSize is None:
propID = mwd5mn["id"] if shipPower > mwd5mn["powerReq"] else None propID = mwd5mn["id"] if shipPower > mwd5mn["powerReq"] else None
elif rigSize == RigSize.MEDIUM: elif rigSize == PortEftRigSize.MEDIUM:
propID = mwd50mn["id"] if shipPower > mwd50mn["powerReq"] else mwd5mn["id"] propID = mwd50mn["id"] if shipPower > mwd50mn["powerReq"] else mwd5mn["id"]
elif rigSize == RigSize.LARGE: elif rigSize == PortEftRigSize.LARGE:
propID = mwd500mn["id"] if shipPower > mwd500mn["powerReq"] else mwd50mn["id"] propID = mwd500mn["id"] if shipPower > mwd500mn["powerReq"] else mwd50mn["id"]
elif rigSize == RigSize.CAPITAL: elif rigSize == PortEftRigSize.CAPITAL:
propID = mwd50000mn["id"] if shipPower > mwd50000mn["powerReq"] else mwd500mn["id"] propID = mwd50000mn["id"] if shipPower > mwd50000mn["powerReq"] else mwd500mn["id"]
if propID is None: if propID is None:
@@ -86,7 +79,7 @@ class EfsPort:
propWithBloom = next(filter(activePropWBloomFilter, propMods), None) propWithBloom = next(filter(activePropWBloomFilter, propMods), None)
if propWithBloom is not None: if propWithBloom is not None:
oldPropState = propWithBloom.state oldPropState = propWithBloom.state
propWithBloom.state = State.ONLINE propWithBloom.state = FittingModuleState.ONLINE
sFit.recalc(fit) sFit.recalc(fit)
sp = fit.maxSpeed sp = fit.maxSpeed
sig = fit.ship.getModifiedItemAttr("signatureRadius") sig = fit.ship.getModifiedItemAttr("signatureRadius")
@@ -198,8 +191,8 @@ class EfsPort:
def getModuleInfo(fit, padTypeIDs=False): def getModuleInfo(fit, padTypeIDs=False):
moduleNames = [] moduleNames = []
modTypeIDs = [] modTypeIDs = []
moduleNameSets = {Slot.LOW: [], Slot.MED: [], Slot.HIGH: [], Slot.RIG: [], Slot.SUBSYSTEM: []} moduleNameSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []}
modTypeIDSets = {Slot.LOW: [], Slot.MED: [], Slot.HIGH: [], Slot.RIG: [], Slot.SUBSYSTEM: []} modTypeIDSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []}
for mod in fit.modules: for mod in fit.modules:
try: try:
if mod.item is not None: if mod.item is not None:
@@ -216,17 +209,17 @@ class EfsPort:
pyfalog.error("Could not find name for module {0}".format(vars(mod))) pyfalog.error("Could not find name for module {0}".format(vars(mod)))
for modInfo in [ for modInfo in [
["High Slots:"], moduleNameSets[Slot.HIGH], ["", "Med Slots:"], moduleNameSets[Slot.MED], ["High Slots:"], moduleNameSets[FittingSlot.HIGH], ["", "Med Slots:"], moduleNameSets[FittingSlot.MED],
["", "Low Slots:"], moduleNameSets[Slot.LOW], ["", "Rig Slots:"], moduleNameSets[Slot.RIG] ["", "Low Slots:"], moduleNameSets[FittingSlot.LOW], ["", "Rig Slots:"], moduleNameSets[FittingSlot.RIG]
]: ]:
moduleNames.extend(modInfo) moduleNames.extend(modInfo)
if len(moduleNameSets[Slot.SUBSYSTEM]) > 0: if len(moduleNameSets[FittingSlot.SUBSYSTEM]) > 0:
moduleNames.extend(["", "Subsystems:"]) moduleNames.extend(["", "Subsystems:"])
moduleNames.extend(moduleNameSets[Slot.SUBSYSTEM]) moduleNames.extend(moduleNameSets[FittingSlot.SUBSYSTEM])
for slotType in [Slot.HIGH, Slot.MED, Slot.LOW, Slot.RIG, Slot.SUBSYSTEM]: for slotType in [FittingSlot.HIGH, FittingSlot.MED, FittingSlot.LOW, FittingSlot.RIG, FittingSlot.SUBSYSTEM]:
if slotType is not Slot.SUBSYSTEM or len(modTypeIDSets[slotType]) > 0: if slotType is not FittingSlot.SUBSYSTEM or len(modTypeIDSets[slotType]) > 0:
modTypeIDs.extend([0, 0] if slotType is not Slot.HIGH else [0]) modTypeIDs.extend([0, 0] if slotType is not FittingSlot.HIGH else [0])
modTypeIDs.extend(modTypeIDSets[slotType]) modTypeIDs.extend(modTypeIDSets[slotType])
droneNames = [] droneNames = []
@@ -331,18 +324,18 @@ class EfsPort:
name = stats.item.name + ", " + stats.charge.name name = stats.item.name + ", " + stats.charge.name
else: else:
name = stats.item.name name = stats.item.name
if stats.hardpoint == Hardpoint.TURRET: if stats.hardpoint == FittingHardpoint.TURRET:
tracking = stats.getModifiedItemAttr("trackingSpeed") tracking = stats.getModifiedItemAttr("trackingSpeed")
typeing = "Turret" typeing = "Turret"
# Bombs share most attributes with missiles despite not needing the hardpoint # Bombs share most attributes with missiles despite not needing the hardpoint
elif stats.hardpoint == Hardpoint.MISSILE or "Bomb Launcher" in stats.item.name: elif stats.hardpoint == FittingHardpoint.MISSILE or "Bomb Launcher" in stats.item.name:
maxVelocity = stats.getModifiedChargeAttr("maxVelocity") maxVelocity = stats.getModifiedChargeAttr("maxVelocity")
explosionDelay = stats.getModifiedChargeAttr("explosionDelay") explosionDelay = stats.getModifiedChargeAttr("explosionDelay")
damageReductionFactor = stats.getModifiedChargeAttr("aoeDamageReductionFactor") damageReductionFactor = stats.getModifiedChargeAttr("aoeDamageReductionFactor")
explosionRadius = stats.getModifiedChargeAttr("aoeCloudSize") explosionRadius = stats.getModifiedChargeAttr("aoeCloudSize")
explosionVelocity = stats.getModifiedChargeAttr("aoeVelocity") explosionVelocity = stats.getModifiedChargeAttr("aoeVelocity")
typeing = "Missile" typeing = "Missile"
elif stats.hardpoint == Hardpoint.NONE: elif stats.hardpoint == FittingHardpoint.NONE:
aoeFieldRange = stats.getModifiedItemAttr("empFieldRange") aoeFieldRange = stats.getModifiedItemAttr("empFieldRange")
# This also covers non-bomb weapons with dps values and no hardpoints, most notably targeted doomsdays. # This also covers non-bomb weapons with dps values and no hardpoints, most notably targeted doomsdays.
typeing = "SmartBomb" typeing = "SmartBomb"
@@ -496,11 +489,11 @@ class EfsPort:
getDroneMulti = lambda d: sumDamage(d.getModifiedItemAttr) * d.getModifiedItemAttr("damageMultiplier") getDroneMulti = lambda d: sumDamage(d.getModifiedItemAttr) * d.getModifiedItemAttr("damageMultiplier")
fitMultipliers["drones"] = list(map(getDroneMulti, tf.drones)) fitMultipliers["drones"] = list(map(getDroneMulti, tf.drones))
getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == Hardpoint.TURRET, f.modules) getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.TURRET, f.modules)
getTurretMulti = lambda mod: mod.getModifiedItemAttr("damageMultiplier") / mod.cycleTime getTurretMulti = lambda mod: mod.getModifiedItemAttr("damageMultiplier") / mod.cycleTime
fitMultipliers["turrets"] = list(map(getTurretMulti, getFitTurrets(tf))) fitMultipliers["turrets"] = list(map(getTurretMulti, getFitTurrets(tf)))
getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == Hardpoint.MISSILE, f.modules) getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.MISSILE, f.modules)
getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.cycleTime getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.cycleTime
fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf))) fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf)))
return fitMultipliers return fitMultipliers
@@ -538,7 +531,7 @@ class EfsPort:
if effect._Effect__effectModule is not None: if effect._Effect__effectModule is not None:
effect.handler(tf, fit.mode, []) effect.handler(tf, fit.mode, [])
if fit.ship.item.groupID == getGroup("Strategic Cruiser").ID: if fit.ship.item.groupID == getGroup("Strategic Cruiser").ID:
subSystems = list(filter(lambda mod: mod.slot == Slot.SUBSYSTEM and mod.item, fit.modules)) subSystems = list(filter(lambda mod: mod.slot == FittingSlot.SUBSYSTEM and mod.item, fit.modules))
for sub in subSystems: for sub in subSystems:
for effect in sub.item.effects.values(): for effect in sub.item.effects.values():
if effect._Effect__effectModule is not None: if effect._Effect__effectModule is not None:
@@ -583,7 +576,7 @@ class EfsPort:
return sizeNotFoundMsg return sizeNotFoundMsg
@staticmethod @staticmethod
def exportEfs(fit, typeNotFitFlag): def exportEfs(fit, typeNotFitFlag, callback):
sFit = Fit.getInstance() sFit = Fit.getInstance()
includeShipTypeData = typeNotFitFlag > 0 includeShipTypeData = typeNotFitFlag > 0
if includeShipTypeData: if includeShipTypeData:
@@ -680,4 +673,8 @@ class EfsPort:
pyfalog.error(e) pyfalog.error(e)
dataDict = {"name": fitName + "Fit could not be correctly parsed"} dataDict = {"name": fitName + "Fit could not be correctly parsed"}
export = json.dumps(dataDict, skipkeys=True) export = json.dumps(dataDict, skipkeys=True)
return export
if callback:
callback(export)
else:
return export

View File

@@ -19,7 +19,6 @@
import re import re
from enum import Enum
from logbook import Logger from logbook import Logger
@@ -30,9 +29,11 @@ from eos.saveddata.booster import Booster
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter from eos.saveddata.fighter import Fighter
from eos.saveddata.implant import Implant from eos.saveddata.implant import Implant
from eos.saveddata.module import Module, State, Slot from eos.saveddata.module import Module
from eos.saveddata.ship import Ship from eos.saveddata.ship import Ship
from eos.saveddata.fit import Fit from eos.saveddata.fit import Fit
from eos.const import FittingSlot, FittingModuleState
from service.const import PortEftOptions, PortEftRigSize
from service.fit import Fit as svcFit from service.fit import Fit as svcFit
from service.market import Market from service.market import Market
from service.port.muta import parseMutant, renderMutant from service.port.muta import parseMutant, renderMutant
@@ -41,26 +42,19 @@ from service.port.shared import IPortUser, fetchItem, processing_notify
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
class Options(Enum):
IMPLANTS = 1
MUTATIONS = 2
LOADED_CHARGES = 3
EFT_OPTIONS = ( EFT_OPTIONS = (
(Options.LOADED_CHARGES.value, 'Loaded Charges', 'Export charges loaded into modules', True), (PortEftOptions.LOADED_CHARGES, 'Loaded Charges', 'Export charges loaded into modules', True),
(Options.MUTATIONS.value, 'Mutated Attributes', 'Export mutated modules\' stats', True), (PortEftOptions.MUTATIONS, 'Mutated Attributes', 'Export mutated modules\' stats', True),
(Options.IMPLANTS.value, 'Implants && Boosters', 'Export implants and boosters', True), (PortEftOptions.IMPLANTS, 'Implants && Boosters', 'Export implants and boosters', True),
) )
MODULE_CATS = ('Module', 'Subsystem', 'Structure Module') MODULE_CATS = ('Module', 'Subsystem', 'Structure Module')
SLOT_ORDER = (Slot.LOW, Slot.MED, Slot.HIGH, Slot.RIG, Slot.SUBSYSTEM, Slot.SERVICE) SLOT_ORDER = (FittingSlot.LOW, FittingSlot.MED, FittingSlot.HIGH, FittingSlot.RIG, FittingSlot.SUBSYSTEM, FittingSlot.SERVICE)
OFFLINE_SUFFIX = '/OFFLINE' OFFLINE_SUFFIX = '/OFFLINE'
def exportEft(fit, options): def exportEft(fit, options, callback):
# EFT formatted export is split in several sections, each section is # EFT formatted export is split in several sections, each section is
# separated from another using 2 blank lines. Sections might have several # separated from another using 2 blank lines. Sections might have several
# sub-sections, which are separated by 1 blank line # sub-sections, which are separated by 1 blank line
@@ -86,21 +80,21 @@ def exportEft(fit, options):
modName = module.baseItem.name modName = module.baseItem.name
else: else:
modName = module.item.name modName = module.item.name
if module.isMutated and options[Options.MUTATIONS.value]: if module.isMutated and options[PortEftOptions.MUTATIONS]:
mutants[mutantReference] = module mutants[mutantReference] = module
mutationSuffix = ' [{}]'.format(mutantReference) mutationSuffix = ' [{}]'.format(mutantReference)
mutantReference += 1 mutantReference += 1
else: else:
mutationSuffix = '' mutationSuffix = ''
modOfflineSuffix = ' {}'.format(OFFLINE_SUFFIX) if module.state == State.OFFLINE else '' modOfflineSuffix = ' {}'.format(OFFLINE_SUFFIX) if module.state == FittingModuleState.OFFLINE else ''
if module.charge and options[Options.LOADED_CHARGES.value]: if module.charge and options[PortEftOptions.LOADED_CHARGES]:
rackLines.append('{}, {}{}{}'.format( rackLines.append('{}, {}{}{}'.format(
modName, module.charge.name, modOfflineSuffix, mutationSuffix)) modName, module.charge.name, modOfflineSuffix, mutationSuffix))
else: else:
rackLines.append('{}{}{}'.format(modName, modOfflineSuffix, mutationSuffix)) rackLines.append('{}{}{}'.format(modName, modOfflineSuffix, mutationSuffix))
else: else:
rackLines.append('[Empty {} slot]'.format( rackLines.append('[Empty {} slot]'.format(
Slot.getName(slotType).capitalize() if slotType is not None else '')) FittingSlot(slotType).name.capitalize() if slotType is not None else ''))
if rackLines: if rackLines:
modSection.append('\n'.join(rackLines)) modSection.append('\n'.join(rackLines))
if modSection: if modSection:
@@ -122,15 +116,15 @@ def exportEft(fit, options):
sections.append('\n\n'.join(minionSection)) sections.append('\n\n'.join(minionSection))
# Section 3: implants, boosters # Section 3: implants, boosters
if options[Options.IMPLANTS.value]: if options[PortEftOptions.IMPLANTS]:
charSection = [] charSection = []
implantLines = [] implantLines = []
for implant in fit.implants: for implant in sorted(fit.implants, key=lambda i: i.slot or 0):
implantLines.append(implant.item.name) implantLines.append(implant.item.name)
if implantLines: if implantLines:
charSection.append('\n'.join(implantLines)) charSection.append('\n'.join(implantLines))
boosterLines = [] boosterLines = []
for booster in fit.boosters: for booster in sorted(fit.boosters, key=lambda b: b.slot or 0):
boosterLines.append(booster.item.name) boosterLines.append(booster.item.name)
if boosterLines: if boosterLines:
charSection.append('\n'.join(boosterLines)) charSection.append('\n'.join(boosterLines))
@@ -149,14 +143,19 @@ def exportEft(fit, options):
# Section 5: mutated modules' details # Section 5: mutated modules' details
mutationLines = [] mutationLines = []
if mutants and options[Options.MUTATIONS.value]: if mutants and options[PortEftOptions.MUTATIONS]:
for mutantReference in sorted(mutants): for mutantReference in sorted(mutants):
mutant = mutants[mutantReference] mutant = mutants[mutantReference]
mutationLines.append(renderMutant(mutant, firstPrefix='[{}] '.format(mutantReference), prefix=' ')) mutationLines.append(renderMutant(mutant, firstPrefix='[{}] '.format(mutantReference), prefix=' '))
if mutationLines: if mutationLines:
sections.append('\n'.join(mutationLines)) sections.append('\n'.join(mutationLines))
return '{}\n\n{}'.format(header, '\n\n\n'.join(sections)) text = '{}\n\n{}'.format(header, '\n\n\n'.join(sections))
if callback:
callback(text)
else:
return text
def importEft(lines): def importEft(lines):
@@ -441,8 +440,8 @@ def importEftCfg(shipname, lines, iportuser):
else: else:
m.owner = fitobj m.owner = fitobj
# Activate mod if it is activable # Activate mod if it is activable
if m.isValidState(State.ACTIVE): if m.isValidState(FittingModuleState.ACTIVE):
m.state = State.ACTIVE m.state = FittingModuleState.ACTIVE
# Add charge to mod if applicable, on any errors just don't add anything # Add charge to mod if applicable, on any errors just don't add anything
if chargeName: if chargeName:
try: try:
@@ -722,12 +721,12 @@ class AbstractFit:
@property @property
def __slotContainerMap(self): def __slotContainerMap(self):
return { return {
Slot.HIGH: self.modulesHigh, FittingSlot.HIGH: self.modulesHigh,
Slot.MED: self.modulesMed, FittingSlot.MED: self.modulesMed,
Slot.LOW: self.modulesLow, FittingSlot.LOW: self.modulesLow,
Slot.RIG: self.rigs, FittingSlot.RIG: self.rigs,
Slot.SUBSYSTEM: self.subsystems, FittingSlot.SUBSYSTEM: self.subsystems,
Slot.SERVICE: self.services} FittingSlot.SERVICE: self.services}
def getContainerBySlot(self, slotType): def getContainerBySlot(self, slotType):
return self.__slotContainerMap.get(slotType) return self.__slotContainerMap.get(slotType)
@@ -798,10 +797,10 @@ class AbstractFit:
if itemSpec.charge is not None and m.isValidCharge(itemSpec.charge): if itemSpec.charge is not None and m.isValidCharge(itemSpec.charge):
m.charge = itemSpec.charge m.charge = itemSpec.charge
if itemSpec.offline and m.isValidState(State.OFFLINE): if itemSpec.offline and m.isValidState(FittingModuleState.OFFLINE):
m.state = State.OFFLINE m.state = FittingModuleState.OFFLINE
elif m.isValidState(State.ACTIVE): elif m.isValidState(FittingModuleState.ACTIVE):
m.state = State.ACTIVE m.state = FittingModuleState.ACTIVE
return m return m
def addImplant(self, itemSpec): def addImplant(self, itemSpec):

View File

@@ -28,7 +28,8 @@ from eos.saveddata.citadel import Citadel
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter from eos.saveddata.fighter import Fighter
from eos.saveddata.fit import Fit from eos.saveddata.fit import Fit
from eos.saveddata.module import Module, State, Slot from eos.saveddata.module import Module
from eos.const import FittingSlot, FittingModuleState
from eos.saveddata.ship import Ship from eos.saveddata.ship import Ship
from service.fit import Fit as svcFit from service.fit import Fit as svcFit
from service.market import Market from service.market import Market
@@ -41,12 +42,12 @@ class ESIExportException(Exception):
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
INV_FLAGS = { INV_FLAGS = {
Slot.LOW: 11, FittingSlot.LOW: 11,
Slot.MED: 19, FittingSlot.MED: 19,
Slot.HIGH: 27, FittingSlot.HIGH: 27,
Slot.RIG: 92, FittingSlot.RIG: 92,
Slot.SUBSYSTEM: 125, FittingSlot.SUBSYSTEM: 125,
Slot.SERVICE: 164 FittingSlot.SERVICE: 164
} }
INV_FLAG_CARGOBAY = 5 INV_FLAG_CARGOBAY = 5
@@ -54,7 +55,7 @@ INV_FLAG_DRONEBAY = 87
INV_FLAG_FIGHTER = 158 INV_FLAG_FIGHTER = 158
def exportESI(ofit): def exportESI(ofit, callback):
# A few notes: # A few notes:
# max fit name length is 50 characters # max fit name length is 50 characters
# Most keys are created simply because they are required, but bogus data is okay # Most keys are created simply because they are required, but bogus data is okay
@@ -82,7 +83,7 @@ def exportESI(ofit):
item = nested_dict() item = nested_dict()
slot = module.slot slot = module.slot
if slot == Slot.SUBSYSTEM: if slot == FittingSlot.SUBSYSTEM:
# Order of subsystem matters based on this attr. See GH issue #130 # Order of subsystem matters based on this attr. See GH issue #130
slot = int(module.getModifiedItemAttr("subSystemSlot")) slot = int(module.getModifiedItemAttr("subSystemSlot"))
item['flag'] = slot item['flag'] = slot
@@ -134,7 +135,12 @@ def exportESI(ofit):
if len(fit['items']) == 0: if len(fit['items']) == 0:
raise ESIExportException("Cannot export fitting: module list cannot be empty.") raise ESIExportException("Cannot export fitting: module list cannot be empty.")
return json.dumps(fit) text = json.dumps(fit)
if callback:
callback(text)
else:
return text
def importESI(string): def importESI(string):
@@ -189,8 +195,8 @@ def importESI(string):
if m.fits(fitobj): if m.fits(fitobj):
fitobj.modules.append(m) fitobj.modules.append(m)
else: else:
if m.isValidState(State.ACTIVE): if m.isValidState(FittingModuleState.ACTIVE):
m.state = State.ACTIVE m.state = FittingModuleState.ACTIVE
moduleList.append(m) moduleList.append(m)

View File

@@ -18,60 +18,77 @@
# ============================================================================= # =============================================================================
from enum import Enum from service.const import PortMultiBuyOptions
from service.price import Price as sPrc
class Options(Enum):
IMPLANTS = 1
CARGO = 2
LOADED_CHARGES = 3
MULTIBUY_OPTIONS = ( MULTIBUY_OPTIONS = (
(Options.LOADED_CHARGES.value, 'Loaded Charges', 'Export charges loaded into modules', True), (PortMultiBuyOptions.LOADED_CHARGES, 'Loaded Charges', 'Export charges loaded into modules', True),
(Options.IMPLANTS.value, 'Implants && Boosters', 'Export implants and boosters', False), (PortMultiBuyOptions.IMPLANTS, 'Implants && Boosters', 'Export implants and boosters', False),
(Options.CARGO.value, 'Cargo', 'Export cargo contents', True), (PortMultiBuyOptions.CARGO, 'Cargo', 'Export cargo contents', True),
(PortMultiBuyOptions.OPTIMIZE_PRICES, 'Optimize Prices', 'Replace items by cheaper alternatives', False),
) )
def exportMultiBuy(fit, options): def exportMultiBuy(fit, options, callback):
itemCounts = {} itemAmounts = {}
def addItem(item, quantity=1):
if item not in itemCounts:
itemCounts[item] = 0
itemCounts[item] += quantity
for module in fit.modules: for module in fit.modules:
if module.item: if module.item:
# Mutated items are of no use for multibuy # Mutated items are of no use for multibuy
if module.isMutated: if module.isMutated:
continue continue
addItem(module.item) _addItem(itemAmounts, module.item)
if module.charge and options[Options.LOADED_CHARGES.value]: if module.charge and options[PortMultiBuyOptions.LOADED_CHARGES]:
addItem(module.charge, module.numCharges) _addItem(itemAmounts, module.charge, module.numCharges)
for drone in fit.drones: for drone in fit.drones:
addItem(drone.item, drone.amount) _addItem(itemAmounts, drone.item, drone.amount)
for fighter in fit.fighters: for fighter in fit.fighters:
addItem(fighter.item, fighter.amountActive) _addItem(itemAmounts, fighter.item, fighter.amountActive)
if options[Options.CARGO.value]: if options[PortMultiBuyOptions.CARGO]:
for cargo in fit.cargo: for cargo in fit.cargo:
addItem(cargo.item, cargo.amount) _addItem(itemAmounts, cargo.item, cargo.amount)
if options[Options.IMPLANTS.value]: if options[PortMultiBuyOptions.IMPLANTS]:
for implant in fit.implants: for implant in fit.implants:
addItem(implant.item) _addItem(itemAmounts, implant.item)
for booster in fit.boosters: for booster in fit.boosters:
addItem(booster.item) _addItem(itemAmounts, booster.item)
if options[PortMultiBuyOptions.OPTIMIZE_PRICES]:
def formatCheaperExportCb(replacementsCheaper):
updatedAmounts = {}
for item, itemAmount in itemAmounts.items():
_addItem(updatedAmounts, replacementsCheaper.get(item, item), itemAmount)
string = _prepareString(fit.ship.item, updatedAmounts)
callback(string)
priceSvc = sPrc.getInstance()
priceSvc.findCheaperReplacements(itemAmounts, formatCheaperExportCb)
else:
string = _prepareString(fit.ship.item, itemAmounts)
if callback:
callback(string)
else:
return string
def _addItem(container, item, quantity=1):
if item not in container:
container[item] = 0
container[item] += quantity
def _prepareString(shipItem, itemAmounts):
exportLines = [] exportLines = []
exportLines.append(fit.ship.item.name) exportLines.append(shipItem.name)
for item in sorted(itemCounts, key=lambda i: (i.group.category.name, i.group.name, i.name)): for item in sorted(itemAmounts, key=lambda i: (i.group.category.name, i.group.name, i.name)):
count = itemCounts[item] count = itemAmounts[item]
if count == 1: if count == 1:
exportLines.append(item.name) exportLines.append(item.name)
else: else:

View File

@@ -29,7 +29,7 @@ from bs4 import UnicodeDammit
from logbook import Logger from logbook import Logger
from eos import db from eos import db
from eos.saveddata.fit import ImplantLocation from eos.const import ImplantLocation
from service.fit import Fit as svcFit from service.fit import Fit as svcFit
from service.port.dna import exportDna, importDna from service.port.dna import exportDna, importDna
from service.port.eft import exportEft, importEft, importEftCfg from service.port.eft import exportEft, importEft, importEftCfg
@@ -257,8 +257,8 @@ class Port(object):
return importEftCfg(shipname, lines, iportuser) return importEftCfg(shipname, lines, iportuser)
@classmethod @classmethod
def exportEft(cls, fit, options): def exportEft(cls, fit, options, callback=None):
return exportEft(fit, options) return exportEft(fit, options, callback=callback)
# DNA-related methods # DNA-related methods
@staticmethod @staticmethod
@@ -266,8 +266,8 @@ class Port(object):
return importDna(string) return importDna(string)
@staticmethod @staticmethod
def exportDna(fit): def exportDna(fit, callback=None):
return exportDna(fit) return exportDna(fit, callback=callback)
# ESI-related methods # ESI-related methods
@staticmethod @staticmethod
@@ -275,8 +275,8 @@ class Port(object):
return importESI(string) return importESI(string)
@staticmethod @staticmethod
def exportESI(fit): def exportESI(fit, callback=None):
return exportESI(fit) return exportESI(fit, callback=callback)
# XML-related methods # XML-related methods
@staticmethod @staticmethod
@@ -284,10 +284,10 @@ class Port(object):
return importXml(text, iportuser) return importXml(text, iportuser)
@staticmethod @staticmethod
def exportXml(iportuser=None, *fits): def exportXml(iportuser=None, callback=None, *fits):
return exportXml(iportuser, *fits) return exportXml(iportuser, callback=callback, *fits)
# Multibuy-related methods # Multibuy-related methods
@staticmethod @staticmethod
def exportMultiBuy(fit, options): def exportMultiBuy(fit, options, callback=None):
return exportMultiBuy(fit, options) return exportMultiBuy(fit, options, callback=callback)

View File

@@ -28,8 +28,9 @@ from eos.saveddata.citadel import Citadel
from eos.saveddata.drone import Drone from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter from eos.saveddata.fighter import Fighter
from eos.saveddata.fit import Fit from eos.saveddata.fit import Fit
from eos.saveddata.module import Module, State, Slot from eos.saveddata.module import Module
from eos.saveddata.ship import Ship from eos.saveddata.ship import Ship
from eos.const import FittingSlot, FittingModuleState
from service.fit import Fit as svcFit from service.fit import Fit as svcFit
from service.market import Market from service.market import Market
from utils.strfunctions import sequential_rep, replace_ltgt from utils.strfunctions import sequential_rep, replace_ltgt
@@ -198,8 +199,8 @@ def importXml(text, iportuser):
m.owner = fitobj m.owner = fitobj
fitobj.modules.append(m) fitobj.modules.append(m)
else: else:
if m.isValidState(State.ACTIVE): if m.isValidState(FittingModuleState.ACTIVE):
m.state = State.ACTIVE m.state = FittingModuleState.ACTIVE
moduleList.append(m) moduleList.append(m)
@@ -225,14 +226,13 @@ def importXml(text, iportuser):
return fit_list return fit_list
def exportXml(iportuser, *fits): def exportXml(iportuser, callback, *fits):
doc = xml.dom.minidom.Document() doc = xml.dom.minidom.Document()
fittings = doc.createElement("fittings") fittings = doc.createElement("fittings")
# fit count # fit count
fit_count = len(fits) fit_count = len(fits)
fittings.setAttribute("count", "%s" % fit_count) fittings.setAttribute("count", "%s" % fit_count)
doc.appendChild(fittings) doc.appendChild(fittings)
sFit = svcFit.getInstance()
for i, fit in enumerate(fits): for i, fit in enumerate(fits):
try: try:
@@ -266,7 +266,7 @@ def exportXml(iportuser, *fits):
slot = module.slot slot = module.slot
if slot == Slot.SUBSYSTEM: if slot == FittingSlot.SUBSYSTEM:
# Order of subsystem matters based on this attr. See GH issue #130 # Order of subsystem matters based on this attr. See GH issue #130
slotId = module.getModifiedItemAttr("subSystemSlot") - 125 slotId = module.getModifiedItemAttr("subSystemSlot") - 125
else: else:
@@ -278,7 +278,7 @@ def exportXml(iportuser, *fits):
hardware = doc.createElement("hardware") hardware = doc.createElement("hardware")
hardware.setAttribute("type", module.item.name) hardware.setAttribute("type", module.item.name)
slotName = Slot.getName(slot).lower() slotName = FittingSlot(slot).name.lower()
slotName = slotName if slotName != "high" else "hi" slotName = slotName if slotName != "high" else "hi"
hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId)) hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId))
fitting.appendChild(hardware) fitting.appendChild(hardware)
@@ -323,4 +323,9 @@ def exportXml(iportuser, *fits):
iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE, iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE,
(i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name)) (i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name))
) )
return doc.toprettyxml() text = doc.toprettyxml()
if callback:
callback(text)
else:
return text

View File

@@ -18,28 +18,26 @@
# ============================================================================= # =============================================================================
import math
import queue import queue
import threading import threading
import time from itertools import chain
import wx import wx
from logbook import Logger from logbook import Logger
from eos import db from eos import db
from eos.saveddata.price import PriceStatus from eos.saveddata.price import PriceStatus
from gui.fitCommands.guiRebaseItems import GuiRebaseItemsCommand
from service.fit import Fit from service.fit import Fit
from service.market import Market from service.market import Market
from service.network import TimeoutError from service.network import TimeoutError
pyfalog = Logger(__name__) pyfalog = Logger(__name__)
VALIDITY = 24 * 60 * 60 # Price validity period, 24 hours class Price:
REREQUEST = 4 * 60 * 60 # Re-request delay for failed fetches, 4 hours
TIMEOUT = 15 * 60 # Network timeout delay for connection issues, 15 minutes
class Price(object):
instance = None instance = None
systemsList = { systemsList = {
@@ -69,38 +67,34 @@ class Price(object):
return cls.instance return cls.instance
@classmethod @classmethod
def fetchPrices(cls, prices): def fetchPrices(cls, prices, fetchTimeout, validityOverride):
"""Fetch all prices passed to this method""" """Fetch all prices passed to this method"""
# Dictionary for our price objects # Dictionary for our price objects
priceMap = {} priceMap = {}
# Check all provided price objects, and add invalid ones to dictionary # Check all provided price objects, and add those we want to update to
# dictionary
for price in prices: for price in prices:
if not price.isValid: if not price.isValid(validityOverride):
priceMap[price.typeID] = price priceMap[price.typeID] = price
if len(priceMap) == 0: if not priceMap:
return return
# Set of items which are still to be requested from this service
toRequest = set()
# Compose list of items we're going to request # Compose list of items we're going to request
for typeID in tuple(priceMap): for typeID in tuple(priceMap):
# Get item object # Get item object
item = db.getItem(typeID) item = db.getItem(typeID)
# We're not going to request items only with market group, as eve-central # We're not going to request items only with market group, as our current market
# doesn't provide any data for items not on the market # sources do not provide any data for items not on the market
if item is None: if item is None:
continue continue
if not item.marketGroupID: if not item.marketGroupID:
priceMap[typeID].status = PriceStatus.notSupported priceMap[typeID].update(PriceStatus.notSupported)
del priceMap[typeID] del priceMap[typeID]
continue continue
toRequest.add(typeID)
# Do not waste our time if all items are not on the market if not priceMap:
if len(toRequest) == 0:
return return
sFit = Fit.getInstance() sFit = Fit.getInstance()
@@ -110,62 +104,46 @@ class Price(object):
return return
# attempt to find user's selected price source, otherwise get first one # attempt to find user's selected price source, otherwise get first one
sourcesToTry = list(cls.sources.keys()) sourceAll = list(cls.sources.keys())
curr = sFit.serviceFittingOptions["priceSource"] if sFit.serviceFittingOptions["priceSource"] in sourcesToTry else sourcesToTry[0] sourcePrimary = sFit.serviceFittingOptions["priceSource"] if sFit.serviceFittingOptions["priceSource"] in sourceAll else sourceAll[0]
while len(sourcesToTry) > 0: # Format: {source name: timeout weight}
sourcesToTry.remove(curr) sources = {sourcePrimary: len(sourceAll)}
for source in sourceAll:
if source == sourcePrimary:
continue
sources[source] = min(sources.values()) - 1
timeoutWeightMult = fetchTimeout / sum(sources.values())
# Record timeouts as it will affect our final decision
timedOutSources = {}
for source, timeoutWeight in sources.items():
pyfalog.info('Trying {}'.format(source))
timedOutSources[source] = False
sourceFetchTimeout = timeoutWeight * timeoutWeightMult
try: try:
sourceCls = cls.sources.get(curr) sourceCls = cls.sources.get(source)
sourceCls(toRequest, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], priceMap) sourceCls(priceMap, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], sourceFetchTimeout)
break
# If getting or processing data returned any errors
except TimeoutError: except TimeoutError:
# Timeout error deserves special treatment pyfalog.warning("Price fetch timeout for source {}".format(source))
pyfalog.warning("Price fetch timout") timedOutSources[source] = True
for typeID in tuple(priceMap): except Exception as e:
priceobj = priceMap[typeID] pyfalog.warn('Failed to fetch prices from price source {}: {}'.format(source, e))
priceobj.time = time.time() + TIMEOUT # Sources remove price map items as they fetch info, if none remain then we're done
priceobj.status = PriceStatus.fail if not priceMap:
del priceMap[typeID] break
except Exception as ex:
# something happened, try another source
pyfalog.warn('Failed to fetch prices from price source {}: {}'.format(curr, ex, sourcesToTry[0]))
if len(sourcesToTry) > 0:
pyfalog.warn('Trying {}'.format(sourcesToTry[0]))
curr = sourcesToTry[0]
# if we get to this point, then we've got an error in all of our sources. Set to REREQUEST delay # If we get to this point, then we've failed to get price with all our sources
for typeID in priceMap.keys(): # If all sources failed due to timeouts, set one status
priceobj = priceMap[typeID] if all(to is True for to in timedOutSources.values()):
priceobj.time = time.time() + REREQUEST for typeID in priceMap.keys():
priceobj.status = PriceStatus.fail priceMap[typeID].update(PriceStatus.fetchTimeout)
# If some sources failed due to any other reason, then it's definitely not network
@classmethod # timeout and we just set another status
def fitItemsList(cls, fit): else:
# Compose a list of all the data we need & request it for typeID in priceMap.keys():
fit_items = [fit.ship.item] priceMap[typeID].update(PriceStatus.fetchFail)
for mod in fit.modules:
if not mod.isEmpty:
fit_items.append(mod.item)
for drone in fit.drones:
fit_items.append(drone.item)
for fighter in fit.fighters:
fit_items.append(fighter.item)
for cargo in fit.cargo:
fit_items.append(cargo.item)
for boosters in fit.boosters:
fit_items.append(boosters.item)
for implants in fit.implants:
fit_items.append(implants.item)
return list(set(fit_items))
def getPriceNow(self, objitem): def getPriceNow(self, objitem):
"""Get price for provided typeID""" """Get price for provided typeID"""
@@ -174,7 +152,7 @@ class Price(object):
return item.price.price return item.price.price
def getPrices(self, objitems, callback, waitforthread=False): def getPrices(self, objitems, callback, fetchTimeout=30, waitforthread=False, validityOverride=None):
"""Get prices for multiple typeIDs""" """Get prices for multiple typeIDs"""
requests = [] requests = []
sMkt = Market.getInstance() sMkt = Market.getInstance()
@@ -186,7 +164,7 @@ class Price(object):
try: try:
callback(requests) callback(requests)
except Exception as e: except Exception as e:
pyfalog.critical("Callback failed.") pyfalog.critical("Execution of callback from getPrices failed.")
pyfalog.critical(e) pyfalog.critical(e)
db.commit() db.commit()
@@ -194,12 +172,43 @@ class Price(object):
if waitforthread: if waitforthread:
self.priceWorkerThread.setToWait(requests, cb) self.priceWorkerThread.setToWait(requests, cb)
else: else:
self.priceWorkerThread.trigger(requests, cb) self.priceWorkerThread.trigger(requests, cb, fetchTimeout, validityOverride)
def clearPriceCache(self): def clearPriceCache(self):
pyfalog.debug("Clearing Prices") pyfalog.debug("Clearing Prices")
db.clearPrices() db.clearPrices()
def findCheaperReplacements(self, items, callback, fetchTimeout=10):
sMkt = Market.getInstance()
replacementsAll = {} # All possible item replacements
for item in items:
if item in replacementsAll:
continue
itemRepls = sMkt.getReplacements(item)
if itemRepls:
replacementsAll[item] = itemRepls
itemsToFetch = {i for i in chain(replacementsAll.keys(), *replacementsAll.values())}
def makeCheapMapCb(requests):
# Decide what we are going to replace
replacementsCheaper = {} # Items which should be replaced
for replacee, replacers in replacementsAll.items():
replacer = min(replacers, key=lambda i: i.price.price or math.inf)
if (replacer.price.price or math.inf) < (replacee.price.price or math.inf):
replacementsCheaper[replacee] = replacer
try:
callback(replacementsCheaper)
except Exception as e:
pyfalog.critical("Execution of callback from findCheaperReplacements failed.")
pyfalog.critical(e)
# Prices older than 2 hours have to be refetched
validityOverride = 2 * 60 * 60
self.getPrices(itemsToFetch, makeCheapMapCb, fetchTimeout=fetchTimeout, validityOverride=validityOverride)
class PriceWorkerThread(threading.Thread): class PriceWorkerThread(threading.Thread):
@@ -214,11 +223,11 @@ class PriceWorkerThread(threading.Thread):
queue = self.queue queue = self.queue
while True: while True:
# Grab our data # Grab our data
callback, requests = queue.get() callback, requests, fetchTimeout, validityOverride = queue.get()
# Grab prices, this is the time-consuming part # Grab prices, this is the time-consuming part
if len(requests) > 0: if len(requests) > 0:
Price.fetchPrices(requests) Price.fetchPrices(requests, fetchTimeout, validityOverride)
wx.CallAfter(callback) wx.CallAfter(callback)
queue.task_done() queue.task_done()
@@ -230,14 +239,13 @@ class PriceWorkerThread(threading.Thread):
for callback in callbacks: for callback in callbacks:
wx.CallAfter(callback) wx.CallAfter(callback)
def trigger(self, prices, callbacks): def trigger(self, prices, callbacks, fetchTimeout, validityOverride):
self.queue.put((callbacks, prices)) self.queue.put((callbacks, prices, fetchTimeout, validityOverride))
def setToWait(self, prices, callback): def setToWait(self, prices, callback):
for x in prices: for price in prices:
if x.typeID not in self.wait: callbacks = self.wait.setdefault(price.typeID, [])
self.wait[x.typeID] = [] callbacks.append(callback)
self.wait[x.typeID].append(callback)
# Import market sources only to initialize price source modules, they register on their own # Import market sources only to initialize price source modules, they register on their own

Some files were not shown because too many files have changed in this diff Show More