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.fit import Fit
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.booster import Booster
@@ -139,7 +140,7 @@ def Saveddata():
'Fit' : Fit,
'Character': Character,
'Module' : Module,
'State' : State,
'State' : FittingModuleState,
'Booster' : Booster,
}
return helper

View File

@@ -6,7 +6,7 @@ import wx
from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, \
StreamHandler, TimedRotatingFileHandler, WARNING
import hashlib
from eos.const import Slot
from eos.const import FittingSlot
from cryptography.fernet import Fernet
@@ -50,11 +50,11 @@ LOGLEVEL_MAP = {
}
slotColourMap = {
Slot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
Slot.MED: wx.Colour(188, 215, 241), # blue = mid slots
Slot.HIGH: wx.Colour(235, 204, 209), # red = high slots
Slot.RIG: '',
Slot.SUBSYSTEM: ''
FittingSlot.LOW: wx.Colour(250, 235, 204), # yellow = low slots
FittingSlot.MED: wx.Colour(188, 215, 241), # blue = mid slots
FittingSlot.HIGH: wx.Colour(235, 204, 209), # red = high slots
FittingSlot.RIG: '',
FittingSlot.SUBSYSTEM: ''
}
def getClientSecret():
@@ -95,7 +95,7 @@ def defPaths(customSavePath=None):
global pyfaPath
global savePath
global saveDB
global gameDB
global gameDB
global saveInRoot
global logPath
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.
saveddata_connectionstring = 'sqlite:///:memory:'
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)
settings = {
"useStaticAdaptiveArmorHardener": False,
"strictSkillLevels": True,
"globalDefaultSpoolupPercentage": 1.0
}
# 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
class Slot(Enum):
@unique
class FittingSlot(IntEnum):
"""
Contains slots for ship fittings
"""
# These are self-explanatory
LOW = 1
MED = 2
@@ -24,3 +46,50 @@ class Slot(Enum):
FS_LIGHT = 13
FS_SUPPORT = 14
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("graphicID", Integer),
Column("groupID", Integer, ForeignKey("invgroups.groupID"), index=True),
Column("replaceSame", String),
Column("replaceBetter", String))
Column("replacements", String))
from .metaGroup import metatypes_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))
mapper(Price, prices_table, properties={
"_Price__price": prices_table.c.price,
})
mapper(Price, prices_table)

View File

@@ -21,6 +21,7 @@ import sys
from sqlalchemy.sql import and_
from sqlalchemy import desc, select
from sqlalchemy import func
from eos.db import saveddata_session, sd_lock
from eos.db.saveddata.fit import projectedFits_table
@@ -283,6 +284,12 @@ def countAllFits():
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):
"""
Get all the fits using a certain ship.

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
#
# Used by:
# 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
type = "active", "projected"
@@ -11,7 +11,7 @@ type = "active", "projected"
def handler(fit, src, context, **kwargs):
amount = 0
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")
if 'effect' in kwargs:

View File

@@ -2,7 +2,7 @@
#
# Used by:
# 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
runTime = "early"
@@ -14,7 +14,7 @@ def handler(fit, module, context):
fit.ship.increaseItemAttr("warpScrambleStatus", module.getModifiedItemAttr("warpScrambleStrength"))
if module.charge is not None and module.charge.ID == 47336:
for mod in fit.modules:
if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > State.ONLINE:
mod.state = State.ONLINE
if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > State.ONLINE:
mod.state = State.ONLINE
if not mod.isEmpty and mod.item.requiresSkill("High Speed Maneuvering") and mod.state > FittingModuleState.ONLINE:
mod.state = FittingModuleState.ONLINE
if not mod.isEmpty and mod.item.requiresSkill("Micro Jump Drive Operation") and mod.state > FittingModuleState.ONLINE:
mod.state = FittingModuleState.ONLINE

View File

@@ -3,15 +3,15 @@
# Used by:
# Module: 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
type = "active", "projected"
def handler(fit, src, context, **kwargs):
if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or
hasattr(src, "amountActive")):
if "projected" in context and ((hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or
hasattr(src, "amountActive")):
amount = src.getModifiedItemAttr("energyNeutralizerAmount")
if 'effect' in kwargs:

View File

@@ -2,15 +2,15 @@
#
# Used by:
# 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
type = "active", "projected"
def handler(fit, src, context, **kwargs):
if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or
hasattr(src, "amountActive")):
if "projected" in context and ((hasattr(src, "state") and src.state >= FittingModuleState.ACTIVE) or
hasattr(src, "amountActive")):
amount = src.getModifiedItemAttr("energyNeutralizerAmount")
time = src.getModifiedItemAttr("energyNeutralizerDuration")

View File

@@ -4,6 +4,7 @@
# Modules from group: Mutadaptive Remote Armor Repairer (5 of 5)
import eos.config
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
repSpoolMax = container.getModifiedItemAttr("repairMultiplierBonusMax")
repSpoolPerCycle = container.getModifiedItemAttr("repairMultiplierBonusPerCycle")
# TODO: fetch spoolup option
defaultSpoolValue = 1
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
spoolType, spoolAmount = resolveSpoolOptions(SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False), container)
rps = repAmountBase * (1 + calculateSpoolup(repSpoolMax, repSpoolPerCycle, cycleTime, spoolType, spoolAmount)[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 eos.graph import Graph
from eos.saveddata.module import State, Hardpoint
from eos.const import FittingModuleState, FittingHardpoint
from logbook import Logger
pyfalog = Logger(__name__)
@@ -46,7 +46,7 @@ class FitDpsGraph(Graph):
abssort = lambda _val: -abs(_val - 1)
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:
ew['signatureRadius'].append(
1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) * self.calculateModuleMultiplier(
@@ -76,12 +76,12 @@ class FitDpsGraph(Graph):
for mod in fit.modules:
dps = mod.getDps(targetResists=fit.targetResists).total
if mod.hardpoint == Hardpoint.TURRET:
if mod.state >= State.ACTIVE:
if mod.hardpoint == FittingHardpoint.TURRET:
if mod.state >= FittingModuleState.ACTIVE:
total += dps * self.calculateTurretMultiplier(mod, data)
elif mod.hardpoint == Hardpoint.MISSILE:
if mod.state >= State.ACTIVE and mod.maxRange is not None and mod.maxRange >= distance:
elif mod.hardpoint == FittingHardpoint.MISSILE:
if mod.state >= FittingModuleState.ACTIVE and mod.maxRange is not None and mod.maxRange >= distance:
total += dps * self.calculateMissileMultiplier(mod, data)
if distance <= fit.extraAttributes["droneControlRange"]:

View File

@@ -30,6 +30,7 @@ pyfalog = Logger(__name__)
class Booster(HandledItem, ItemAttrShortcut):
def __init__(self, item):
self.__item = item
@@ -147,3 +148,17 @@ class Booster(HandledItem, ItemAttrShortcut):
copyEffect.active = sideEffect.active
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.amount = self.amount
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
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):
fitDroneGroupLimits = set()
for i in range(1, 3):

View File

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

View File

@@ -28,28 +28,19 @@ from sqlalchemy.orm import validates, reconstructor
import eos.db
from eos import capSim
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.drone import Drone
from eos.saveddata.character import Character
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 logbook import Logger
pyfalog = Logger(__name__)
class ImplantLocation(Enum):
FIT = 0
CHARACTER = 1
class CalcType(Enum):
LOCAL = 0
PROJECTED = 1
COMMAND = 2
class Fit(object):
"""Represents a fitting, with modules, ship, implants, etc."""
@@ -393,6 +384,32 @@ class Fit(object):
else:
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):
self.__effectiveTank = None
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
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
@@ -901,7 +918,7 @@ class Fit(object):
if self.ship is None:
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)
if amount > 0:
for _ in range(int(amount)):
@@ -948,7 +965,7 @@ class Fit(object):
def getItemAttrOnlineSum(dict, attr):
amount = 0
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:
amount += add
@@ -967,29 +984,29 @@ class Fit(object):
for mod in chain(self.modules, self.fighters):
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
amount += 1
return amount
slots = {
Slot.LOW : "lowSlots",
Slot.MED : "medSlots",
Slot.HIGH : "hiSlots",
Slot.RIG : "rigSlots",
Slot.SUBSYSTEM: "maxSubSystems",
Slot.SERVICE : "serviceSlots",
Slot.F_LIGHT : "fighterLightSlots",
Slot.F_SUPPORT: "fighterSupportSlots",
Slot.F_HEAVY : "fighterHeavySlots",
Slot.FS_LIGHT: "fighterStandupLightSlots",
Slot.FS_SUPPORT: "fighterStandupSupportSlots",
Slot.FS_HEAVY: "fighterStandupHeavySlots",
FittingSlot.LOW : "lowSlots",
FittingSlot.MED : "medSlots",
FittingSlot.HIGH : "hiSlots",
FittingSlot.RIG : "rigSlots",
FittingSlot.SUBSYSTEM: "maxSubSystems",
FittingSlot.SERVICE : "serviceSlots",
FittingSlot.F_LIGHT : "fighterLightSlots",
FittingSlot.F_SUPPORT: "fighterSupportSlots",
FittingSlot.F_HEAVY : "fighterHeavySlots",
FittingSlot.FS_LIGHT: "fighterStandupLightSlots",
FittingSlot.FS_SUPPORT: "fighterStandupSupportSlots",
FittingSlot.FS_HEAVY: "fighterStandupHeavySlots",
}
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
return 0
@@ -1001,12 +1018,12 @@ class Fit(object):
return self.ship.getModifiedItemAttr(self.slots[type]) or 0
def getHardpointsFree(self, type):
if type == Hardpoint.NONE:
if type == FittingHardpoint.NONE:
return 1
elif type == Hardpoint.TURRET:
return self.ship.getModifiedItemAttr('turretSlotsLeft') - self.getHardpointsUsed(Hardpoint.TURRET)
elif type == Hardpoint.MISSILE:
return self.ship.getModifiedItemAttr('launcherSlotsLeft') - self.getHardpointsUsed(Hardpoint.MISSILE)
elif type == FittingHardpoint.TURRET:
return self.ship.getModifiedItemAttr('turretSlotsLeft') - self.getHardpointsUsed(FittingHardpoint.TURRET)
elif type == FittingHardpoint.MISSILE:
return self.ship.getModifiedItemAttr('launcherSlotsLeft') - self.getHardpointsUsed(FittingHardpoint.MISSILE)
else:
raise ValueError("%d is not a valid value for Hardpoint Enum", type)
@@ -1168,7 +1185,7 @@ class Fit(object):
capUsed = 0
capAdded = 0
for mod in self.modules:
if mod.state >= State.ACTIVE:
if mod.state >= FittingModuleState.ACTIVE:
if (mod.getModifiedItemAttr("capacitorNeed") or 0) != 0:
cycleTime = mod.rawCycleTime or 0
reactivationTime = mod.getModifiedItemAttr("moduleReactivationDelay") or 0
@@ -1182,7 +1199,7 @@ class Fit(object):
capAdded -= capNeed
# 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,
mod.numShots or 0, disableStagger, reloadTime))

View File

@@ -115,6 +115,11 @@ class Implant(HandledItem, ItemAttrShortcut):
copy.active = self.active
return copy
def rebase(self, item):
active = self.active
Implant.__init__(self, item)
self.active = active
def __repr__(self):
return "Implant(ID={}, name={}) at {}".format(
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
import eos.db
from eos.const import Slot
from eos.const import FittingModuleState, FittingHardpoint, FittingSlot
from eos.effectHandlerHelpers import HandledCharge, HandledItem
from eos.enum import Enum
from eos.modifiedAttributeDict import ChargeAttrShortcut, ItemAttrShortcut, ModifiedAttributeDict
from eos.saveddata.citadel import Citadel
from eos.saveddata.mutator import Mutator
@@ -35,44 +34,30 @@ from eos.utils.stats import DmgTypes
pyfalog = Logger(__name__)
class State(Enum):
OFFLINE = -1
ONLINE = 0
ACTIVE = 1
OVERHEATED = 2
ProjectedMap = {
State.OVERHEATED: State.ACTIVE,
State.ACTIVE: State.OFFLINE,
State.OFFLINE: State.ACTIVE,
State.ONLINE: State.ACTIVE # Just in case
FittingModuleState.OVERHEATED: FittingModuleState.ACTIVE,
FittingModuleState.ACTIVE: FittingModuleState.OFFLINE,
FittingModuleState.OFFLINE: FittingModuleState.ACTIVE,
FittingModuleState.ONLINE: FittingModuleState.ACTIVE # Just in case
}
# Old state : New State
LocalMap = {
State.OVERHEATED: State.ACTIVE,
State.ACTIVE: State.ONLINE,
State.OFFLINE: State.ONLINE,
State.ONLINE: State.ACTIVE
FittingModuleState.OVERHEATED: FittingModuleState.ACTIVE,
FittingModuleState.ACTIVE: FittingModuleState.ONLINE,
FittingModuleState.OFFLINE: FittingModuleState.ONLINE,
FittingModuleState.ONLINE: FittingModuleState.ACTIVE
}
# For system effects. They should only ever be online or offline
ProjectedSystem = {
State.OFFLINE: State.ONLINE,
State.ONLINE: State.OFFLINE
FittingModuleState.OFFLINE: FittingModuleState.ONLINE,
FittingModuleState.ONLINE: FittingModuleState.OFFLINE
}
class Hardpoint(Enum):
NONE = 0
MISSILE = 1
TURRET = 2
class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
"""An instance of this class represents a module together with its charge and modified attributes"""
MINING_ATTRIBUTES = ("miningAmount",)
@@ -104,7 +89,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__charge = None
self.projected = False
self.state = State.ONLINE
self.state = FittingModuleState.ONLINE
self.build()
@reconstructor
@@ -153,7 +138,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
self.__reloadTime = None
self.__reloadForce = None
self.__chargeCycles = None
self.__hardpoint = Hardpoint.NONE
self.__hardpoint = FittingHardpoint.NONE
self.__itemModifiedAttributes = ModifiedAttributeDict(parent=self)
self.__chargeModifiedAttributes = ModifiedAttributeDict(parent=self)
self.__slot = self.dummySlot # defaults to None
@@ -396,7 +381,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if self.isEmpty:
self.__miningyield = 0
else:
if self.state >= State.ACTIVE:
if self.state >= FittingModuleState.ACTIVE:
volley = self.getModifiedItemAttr("specialtyMiningAmount") or self.getModifiedItemAttr(
"miningAmount") or 0
if volley:
@@ -410,7 +395,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return self.__miningyield
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)
if self.__baseVolley is None:
dmgGetter = self.getModifiedChargeAttr if self.charge else self.getModifiedItemAttr
@@ -448,7 +433,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return dps
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
def getBaseRemoteReps(module):
@@ -554,34 +539,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
return fits
def __fitRestrictions(self, fit, hardpointLimit=True):
# Check ship type restrictions
fitsOnType = set()
fitsOnGroup = set()
shipType = self.getModifiedItemAttr("fitsToShipType", None)
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":
if not fit.canFit(self.item):
return False
# 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
# 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")
for mod in fit.modules:
if mod.getModifiedItemAttr("subSystemSlot") == subSlot:
return False
# Check rig sizes
if self.slot == Slot.RIG:
if self.slot == FittingSlot.RIG:
if self.getModifiedItemAttr("rigSize") != fit.ship.getModifiedItemAttr("rigSize"):
return False
@@ -627,9 +586,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# Check if we're within bounds
if state < -1 or state > 2:
return False
elif state >= State.ACTIVE and not self.item.isType("active"):
elif state >= FittingModuleState.ACTIVE and not self.item.isType("active"):
return False
elif state == State.OVERHEATED and not self.item.isType("overheat"):
elif state == FittingModuleState.OVERHEATED and not self.item.isType("overheat"):
return False
else:
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,
# it should be fine for all cases
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
# 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
for mod in self.owner.modules:
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
if currActive > maxGroupActive:
break
@@ -718,28 +677,28 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@staticmethod
def __calculateHardpoint(item):
effectHardpointMap = {
"turretFitted" : Hardpoint.TURRET,
"launcherFitted": Hardpoint.MISSILE
"turretFitted" : FittingHardpoint.TURRET,
"launcherFitted": FittingHardpoint.MISSILE
}
if item is None:
return Hardpoint.NONE
return FittingHardpoint.NONE
for effectName, slot in effectHardpointMap.items():
if effectName in item.effects:
return slot
return Hardpoint.NONE
return FittingHardpoint.NONE
@staticmethod
def calculateSlot(item):
effectSlotMap = {
"rigSlot" : Slot.RIG,
"loPower" : Slot.LOW,
"medPower" : Slot.MED,
"hiPower" : Slot.HIGH,
"subSystem" : Slot.SUBSYSTEM,
"serviceSlot": Slot.SERVICE
"rigSlot" : FittingSlot.RIG.value,
"loPower" : FittingSlot.LOW.value,
"medPower" : FittingSlot.MED.value,
"hiPower" : FittingSlot.HIGH.value,
"subSystem" : FittingSlot.SUBSYSTEM.value,
"serviceSlot": FittingSlot.SERVICE.value
}
if item is None:
return None
@@ -747,7 +706,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if effectName in item.effects:
return slot
if item.group.name in Module.SYSTEM_GROUPS:
return Slot.SYSTEM
return FittingSlot.SYSTEM
return None
@@ -801,8 +760,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if effect.runTime == runTime and \
effect.activeByDefault and \
(effect.isType("offline") or
(effect.isType("passive") and self.state >= State.ONLINE) or
(effect.isType("active") and self.state >= State.ACTIVE)) and \
(effect.isType("passive") and self.state >= FittingModuleState.ONLINE) or
(effect.isType("active") and self.state >= FittingModuleState.ACTIVE)) and \
(not gang or (gang and effect.isType("gang"))):
chargeContext = ("moduleCharge",)
@@ -815,7 +774,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
effect.handler(fit, self, chargeContext)
if self.item:
if self.state >= State.OVERHEATED:
if self.state >= FittingModuleState.OVERHEATED:
for effect in self.item.effects.values():
if effect.runTime == runTime and \
effect.isType("overheat") \
@@ -828,8 +787,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if effect.runTime == runTime and \
effect.activeByDefault and \
(effect.isType("offline") or
(effect.isType("passive") and self.state >= State.ONLINE) or
(effect.isType("active") and self.state >= State.ACTIVE)) \
(effect.isType("passive") and self.state >= FittingModuleState.ONLINE) or
(effect.isType("active") and self.state >= FittingModuleState.ACTIVE)) \
and ((projected and effect.isType("projected")) or not projected) \
and ((gang and effect.isType("gang")) or not gang):
try:
@@ -904,7 +863,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@property
def capUse(self):
capNeed = self.getModifiedItemAttr("capacitorNeed")
if capNeed and self.state >= State.ACTIVE:
if capNeed and self.state >= FittingModuleState.ACTIVE:
cycleTime = self.cycleTime
if cycleTime > 0:
capUsed = capNeed / (cycleTime / 1000.0)
@@ -916,10 +875,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def getProposedState(mod, click, proposedState=None):
# todo: instead of passing in module, make this a instanced function.
pyfalog.debug("Get proposed state for module.")
if mod.slot == Slot.SUBSYSTEM or mod.isEmpty:
return State.ONLINE
if mod.slot == FittingSlot.SUBSYSTEM or mod.isEmpty:
return FittingModuleState.ONLINE
if mod.slot == Slot.SYSTEM:
if mod.slot == FittingSlot.SYSTEM:
transitionMap = ProjectedSystem
else:
transitionMap = ProjectedMap if mod.projected else LocalMap
@@ -929,9 +888,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if proposedState is not None:
state = proposedState
elif click == "right":
state = State.OVERHEATED
state = FittingModuleState.OVERHEATED
elif click == "ctrl":
state = State.OFFLINE
state = FittingModuleState.OFFLINE
else:
state = transitionMap[currState]
if not mod.isValidState(state):
@@ -956,6 +915,16 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
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):
if self.item:
return "Module(ID={}, name={}) at {}".format(

View File

@@ -19,41 +19,56 @@
# ===============================================================================
import time
from enum import IntEnum, unique
from time import time
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__)
@unique
class PriceStatus(IntEnum):
notFetched = 0
success = 1
fail = 2
notSupported = 3
initialized = 0
notSupported = 1
fetchSuccess = 2
fetchFail = 3
fetchTimeout = 4
class Price(object):
def __init__(self, typeID):
self.typeID = typeID
self.time = 0
self.__price = 0
self.status = PriceStatus.notFetched
self.price = 0
self.status = PriceStatus.initialized
@property
def isValid(self):
return self.time >= time.time()
@property
def price(self):
if self.status != PriceStatus.success:
return 0
def isValid(self, validityOverride=None):
# Always attempt to update prices which were just initialized, and prices
# of unsupported items (maybe we start supporting them at some point)
if self.status in (PriceStatus.initialized, PriceStatus.notSupported):
return False
elif self.status == PriceStatus.fetchSuccess:
return time() <= self.time + (validityOverride if validityOverride is not None else VALIDITY)
elif self.status == PriceStatus.fetchFail:
return time() <= self.time + REREQUEST
elif self.status == PriceStatus.fetchTimeout:
return time() <= self.time + TIMEOUT
else:
return self.__price or 0
return False
@price.setter
def price(self, price):
self.__price = price
def update(self, status, price=0):
# Keep old price if we failed to fetch new one
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 enum import IntEnum, unique
from eos.const import SpoolType
from eos.utils.float import floatUnerr
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):
"""
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.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:
self.lastFitId = event.fitID

View File

@@ -81,7 +81,9 @@ class CargoView(d.Display):
if data[0] == "fitting":
self.swapModule(x, y, int(data[1]))
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):
row = event.GetIndex()

View File

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

View File

@@ -26,7 +26,7 @@ from gui.builtinViewColumns.state import State
from gui.utils.staticHelpers import DragDropHelper
from gui.contextMenu import ContextMenu
import gui.globalEvents as GE
from eos.saveddata.fit import ImplantLocation
from eos.const import ImplantLocation
from service.fit import Fit
from service.market import Market
import gui.fitCommands as cmd
@@ -204,7 +204,10 @@ class ImplantDisplay(d.Display):
def removeImplant(self, implant):
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):
event.Skip()

View File

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

View File

@@ -5,7 +5,7 @@ import wx
import gui.fitCommands as cmd
import gui.mainFrame
from eos.saveddata.module import Hardpoint
from eos.const import FittingHardpoint
from gui.bitmap_loader import BitmapLoader
from gui.contextMenu import ContextMenu
from service.market import Market
@@ -136,7 +136,7 @@ class ModuleAmmoPicker(ContextMenu):
hardpoint = self.module.hardpoint
moduleName = self.module.item.name
# 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")
items = []
range_ = None
@@ -180,7 +180,7 @@ class ModuleAmmoPicker(ContextMenu):
m.Append(item)
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)
type_ = 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
# 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()
from service.const import GuiAttrGroup
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
AttrGroupDict = {
AttrGroup.FITTING : {
GuiAttrGroup.FITTING : {
"label" : "Fitting",
"attributes": [
# parent-level attributes
@@ -67,7 +49,7 @@ AttrGroupDict = {
# "mass",
]
},
AttrGroup.STRUCTURE : {
GuiAttrGroup.STRUCTURE : {
"label" : "Structure",
"attributes": [
"hp",
@@ -97,7 +79,7 @@ AttrGroupDict = {
"explosiveDamageResonance"
]
},
AttrGroup.ARMOR : {
GuiAttrGroup.ARMOR : {
"label": "Armor",
"attributes":[
"armorHP",
@@ -109,7 +91,7 @@ AttrGroupDict = {
]
},
AttrGroup.SHIELD : {
GuiAttrGroup.SHIELD : {
"label": "Shield",
"attributes": [
"shieldCapacity",
@@ -122,7 +104,7 @@ AttrGroupDict = {
]
},
AttrGroup.EWAR_RESISTS : {
GuiAttrGroup.EWAR_RESISTS : {
"label": "Electronic Warfare",
"attributes": [
"ECMResistance",
@@ -135,14 +117,14 @@ AttrGroupDict = {
"weaponDisruptionResistance",
]
},
AttrGroup.CAPACITOR : {
GuiAttrGroup.CAPACITOR : {
"label": "Capacitor",
"attributes": [
"capacitorCapacity",
"rechargeRate",
]
},
AttrGroup.TARGETING : {
GuiAttrGroup.TARGETING : {
"label": "Targeting",
"attributes": [
"maxTargetRange",
@@ -160,7 +142,7 @@ AttrGroupDict = {
"scanLadarStrength",
]
},
AttrGroup.SHARED_FACILITIES : {
GuiAttrGroup.SHARED_FACILITIES : {
"label" : "Shared Facilities",
"attributes": [
"fleetHangarCapacity",
@@ -168,7 +150,7 @@ AttrGroupDict = {
"maxJumpClones",
]
},
AttrGroup.FIGHTER_FACILITIES: {
GuiAttrGroup.FIGHTER_FACILITIES: {
"label": "Fighter Squadron Facilities",
"attributes": [
"fighterCapacity",
@@ -181,7 +163,7 @@ AttrGroupDict = {
"fighterStandupHeavySlots",
]
},
AttrGroup.ON_DEATH : {
GuiAttrGroup.ON_DEATH : {
"label": "On Death",
"attributes": [
"onDeathDamageEM",
@@ -192,7 +174,7 @@ AttrGroupDict = {
"onDeathSignatureRadius",
]
},
AttrGroup.JUMP_SYSTEMS : {
GuiAttrGroup.JUMP_SYSTEMS : {
"label": "Jump Drive Systems",
"attributes": [
"jumpDriveCapacitorNeed",
@@ -206,13 +188,13 @@ AttrGroupDict = {
"jumpPortalDuration",
]
},
AttrGroup.PROPULSIONS : {
GuiAttrGroup.PROPULSIONS : {
"label": "Propulsion",
"attributes": [
"maxVelocity"
]
},
AttrGroup.FIGHTERS : {
GuiAttrGroup.FIGHTERS : {
"label": "Fighter",
"attributes": [
"mass",
@@ -225,28 +207,36 @@ AttrGroupDict = {
"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 = [
AttrGroup.FITTING,
AttrGroup.STRUCTURE,
AttrGroup.ARMOR,
AttrGroup.SHIELD,
AttrGroup.EWAR_RESISTS,
AttrGroup.CAPACITOR,
AttrGroup.TARGETING,
AttrGroup.SHARED_FACILITIES,
AttrGroup.FIGHTER_FACILITIES,
AttrGroup.ON_DEATH,
AttrGroup.JUMP_SYSTEMS,
AttrGroup.PROPULSIONS,
GuiAttrGroup.FITTING,
GuiAttrGroup.STRUCTURE,
GuiAttrGroup.ARMOR,
GuiAttrGroup.SHIELD,
GuiAttrGroup.EWAR_RESISTS,
GuiAttrGroup.CAPACITOR,
GuiAttrGroup.TARGETING,
GuiAttrGroup.SHARED_FACILITIES,
GuiAttrGroup.FIGHTER_FACILITIES,
GuiAttrGroup.ON_DEATH,
GuiAttrGroup.JUMP_SYSTEMS,
GuiAttrGroup.PROPULSIONS,
GuiAttrGroup.SHIP_GROUP
]
CategoryGroups = {
"Fighter" : [
AttrGroup.FIGHTERS,
AttrGroup.SHIELD,
AttrGroup.TARGETING,
GuiAttrGroup.FIGHTERS,
GuiAttrGroup.SHIELD,
GuiAttrGroup.TARGETING,
],
"Ship" : Group1,
"Drone" : Group1,

View File

@@ -4,12 +4,12 @@ import config
# noinspection PyPackageRequirements
import wx
import wx.lib.agw.hypertreelist
from gui.builtinItemStatsViews.helpers import AutoListCtrl
from gui.bitmap_loader import BitmapLoader
from gui.utils.numberFormatter import formatAmount, roundDec
from enum import IntEnum
from gui.builtinItemStatsViews.attributeGrouping import *
from service.const import GuiAttrGroup
class AttributeView(IntEnum):
@@ -19,7 +19,8 @@ class AttributeView(IntEnum):
class ItemParams(wx.Panel):
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)
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)
self.SetSizer(mainSizer)
self.toggleView = 1
self.toggleView = AttributeView.NORMAL
self.stuff = stuff
self.item = item
self.attrInfo = {}
@@ -40,9 +41,6 @@ class ItemParams(wx.Panel):
if self.stuff is not None:
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)
mainSizer.Add(self.m_staticline, 0, wx.EXPAND)
bSizer = wx.BoxSizer(wx.HORIZONTAL)
@@ -62,6 +60,8 @@ class ItemParams(wx.Panel):
mainSizer.Add(bSizer, 0, wx.ALIGN_RIGHT)
self.imageList = wx.ImageList(16, 16)
self.PopulateList()
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):
display = None
if isinstance(attr, tuple):
display = attr[1]
attr = attr[0]
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:
return
@@ -188,14 +202,14 @@ class ItemParams(wx.Panel):
def PopulateList(self):
# self.paramList.setResizeColumn(0)
self.imageList = wx.ImageList(16, 16)
self.SetupImageList()
self.processed_attribs = set()
root = self.paramList.AddRoot("The Root Item")
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
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
for data in [AttrGroupDict[o] for o in order]:
heading = data.get("label")
@@ -243,10 +257,14 @@ class ItemParams(wx.Panel):
self.AddAttribute(root, name)
self.paramList.AssignImageList(self.imageList)
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, displayOveride = None):
info = self.attrInfo.get(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):
return None
if info and info.displayName and self.toggleView == 1:
attrName = info.displayName
if info and info.displayName and self.toggleView == AttributeView.NORMAL:
attrName = displayOveride or info.displayName
else:
attrName = attr
@@ -278,27 +296,27 @@ class ItemParams(wx.Panel):
icon = BitmapLoader.getBitmap(iconFile, "icons")
if icon is None:
icon = BitmapLoader.getBitmap("transparent16x16", "gui")
attrIcon = self.imageList.Add(icon)
attrIcon = self.blank_icon
else:
attrIcon = self.imageList.Add(icon)
else:
attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons"))
attrIcon = self.unknown_icon
else:
attrIcon = self.imageList.Add(BitmapLoader.getBitmap("0", "icons"))
attrIcon = self.unknown_icon
# index = self.paramList.AppendItem(root, attrName)
# idNameMap[idCount] = attrName
# self.paramList.SetPyData(index, idCount)
# idCount += 1
if self.toggleView != 1:
if self.toggleView == AttributeView.RAW:
valueUnit = str(value)
elif info and info.unit:
valueUnit = self.FormatValue(*info.unit.PreformatValue(value))
else:
valueUnit = formatAmount(value, 3, 0, 0)
if self.toggleView != 1:
if self.toggleView == AttributeView.RAW:
valueUnitDefault = str(valueDefault)
elif info and info.unit:
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(486) # 200mm AutoCannon I
#item = eos.db.getItem(200) # Phased Plasma L
super().__init__(None, title="Test Attribute Window | {} - {}".format(item.ID, item.name), size=(1000, 500))
if 'wxMSW' in wx.PlatformInfo:
@@ -359,6 +378,7 @@ if __name__ == "__main__":
main_sizer.Add(panel, 1, wx.EXPAND | wx.ALL, 2)
self.SetSizer(main_sizer)
self.Layout()
app = wx.App(redirect=False) # Error messages go to popup window
top = Frame()

View File

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

View File

@@ -43,8 +43,12 @@ class ItemMutator(wx.Panel):
self.badColor = wx.Colour(255, 64, 0)
self.event_mapping = {}
higOverrides = {
('Stasis Web', 'speedFactor'): False,
}
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]
range1 = (m.minValue, m.attribute.unit.SimplifyValue(m.minValue))
range2 = (m.maxValue, m.attribute.unit.SimplifyValue(m.maxValue))
@@ -59,7 +63,7 @@ class ItemMutator(wx.Panel):
minRange = range2
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
worseRange = maxRange
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.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.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.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.rbBox5 = wx.RadioBox(panel, -1, "Charge", wx.DefaultPosition, wx.DefaultSize, ['Disabled', 'Enabled'], 1, wx.RA_SPECIFY_COLS)
self.rbBox5.SetSelection(self.settings.get('moduleAmmoPicker'))
rbSizerRow2.Add(self.rbBox5, 1, wx.ALL, 5)
self.rbBox5.Bind(wx.EVT_RADIOBOX, self.OnSetting5Change)
'''
# 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'))
# rbSizerRow2.Add(self.rbBox5, 0, wx.ALL, 5)
# 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.SetSelection(self.settings.get('moduleGlobalAmmoPicker'))
rbSizerRow2.Add(self.rbBox6, 1, wx.ALL, 5)
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)
# Row 3
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.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.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)
panel.SetSizer(mainSizer)
@@ -115,6 +118,9 @@ class PFContextMenuPref(PreferenceView):
def OnSetting8Change(self, event):
self.settings.set('moduleFill', event.GetInt())
def OnSetting9Change(self, event):
self.settings.set('spoolup', event.GetInt())
def getImage(self):
return BitmapLoader.getBitmap("settings_menu", "gui")

View File

@@ -8,6 +8,7 @@ import gui.globalEvents as GE
from gui.preferenceView import PreferenceView
from service.settings import EOSSettings
import gui.mainFrame
from wx.lib.intctrl import IntCtrl
logger = logging.getLogger(__name__)
@@ -61,6 +62,22 @@ class PFFittingEnginePref(PreferenceView):
wx.DefaultPosition, wx.DefaultSize, 0)
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
'''
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.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.Layout()
def OnSpoolupChange(self, event):
self.engine_settings.set("globalDefaultSpoolupPercentage", self.spoolup_value.GetValue() / 100)
def OnCBGlobalForceReloadStateChange(self, event):
self.sFit.serviceFittingOptions["useGlobalForceReload"] = self.cbGlobalForceReload.GetValue()
fitID = self.mainFrame.getActiveFit()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ import gui.mainFrame
from gui.chrome_tabs import EVT_NOTEBOOK_PAGE_CHANGED
from gui.utils import fonts
from eos.saveddata.module import Hardpoint
from eos.const import FittingHardpoint
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
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%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%sUsedDronesActive", lambda: fit.activeDrones, 0, 0, 0),
("label%sTotalDronesActive", lambda: fit.extraAttributes["maxActiveDrones"], 0, 0, 0),
@@ -278,12 +278,16 @@ class ResourcesViewFull(StatsView):
totalCalibrationPoints = value
labelTCP = label
# See #1877
shown = label.Shown
label.Show(True)
if isinstance(value, str):
label.SetLabel(value)
label.SetToolTip(wx.ToolTip(value))
else:
label.SetLabel(formatAmount(value, prec, lowest, highest))
label.SetToolTip(wx.ToolTip("%.1f" % value))
label.Show(shown)
colorWarn = wx.Colour(204, 51, 51)
colorNormal = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)

View File

@@ -2,8 +2,9 @@
import wx
from eos.saveddata.implant import Implant
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.const import FittingSlot
from gui.viewColumn import ViewColumn
@@ -32,7 +33,7 @@ class BaseIcon(ViewColumn):
return self.shipImage
elif isinstance(stuff, Module):
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")
else:
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.drone import Drone
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.const import FittingSlot
from service.fit import Fit as FitSvc
from service.market import Market
from gui.viewColumn import ViewColumn
@@ -72,10 +73,10 @@ class BaseName(ViewColumn):
return "%s (%s)" % (stuff.name, stuff.ship.item.name)
elif isinstance(stuff, Rack):
if FitSvc.getInstance().serviceFittingOptions["rackLabels"]:
if stuff.slot == Slot.MODE:
if stuff.slot == FittingSlot.MODE:
return '─ Tactical Mode ─'
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:
return ""
elif isinstance(stuff, Module):
@@ -89,7 +90,7 @@ class BaseName(ViewColumn):
return "{} {}".format(type.name, stuff.item.name[-1:])
if stuff.isEmpty:
return "%s Slot" % Slot.getName(stuff.slot).capitalize()
return "%s Slot" % FittingSlot(stuff.slot).name.capitalize()
else:
return stuff.item.name
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.listFormatter import formatList
from eos.utils.spoolSupport import SpoolType, SpoolOptions
import eos.config
class Miscellanea(ViewColumn):
@@ -117,8 +118,8 @@ class Miscellanea(ViewColumn):
text = "{0}".format(formatAmount(trackingSpeed, 3, 0, 3))
tooltip = "tracking speed"
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]
if spoolTime:
text = "{0}s".format(formatAmount(spoolTime, 3, 0, 3))
@@ -339,8 +340,7 @@ class Miscellanea(ViewColumn):
tooltip = "Armor repaired per second"
return text, tooltip
elif itemGroup == "Mutadaptive Remote Armor Repairer":
# TODO: fetch spoolup option
defaultSpoolValue = 1
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
spoolOptDefault = SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)
spoolOptPre = SpoolOptions(SpoolType.SCALE, 0, True)
spoolOptFull = SpoolOptions(SpoolType.SCALE, 1, True)

View File

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

View File

@@ -24,7 +24,8 @@ import wx
from eos.saveddata.fit import Fit
from eos.saveddata.implant import Implant
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
import gui.mainFrame
@@ -46,12 +47,11 @@ class State(ViewColumn):
def getToolTip(self, mod):
if isinstance(mod, Module) and not mod.isEmpty:
return State_.getName(mod.state).title()
return State_(mod.state).name.title()
def getImageId(self, stuff):
generic_active = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(1).lower(), "gui")
generic_inactive = 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_.OFFLINE.name.lower(), "gui")
if isinstance(stuff, Drone):
if stuff.amountActive > 0:
@@ -64,7 +64,7 @@ class State(ViewColumn):
if stuff.isEmpty:
return -1
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")
elif isinstance(stuff, Fit):
fitID = self.mainFrame.getActiveFit()
@@ -83,7 +83,7 @@ class State(ViewColumn):
return generic_inactive
elif isinstance(stuff, Implant) and stuff.character:
# 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:
active = getattr(stuff, "active", None)
if active is None:

View File

@@ -30,7 +30,8 @@ import gui.globalEvents as GE
import gui.mainFrame
import gui.multiSwitch
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.builtinMarketBrowser.events import ITEM_SELECTED
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()
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:
self.mods = fit.modules[:]
@@ -507,7 +515,7 @@ class FittingView(d.Display):
# while also marking the mode header position in the Blanks list
if sFit.serviceFittingOptions["rackSlots"]:
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)
else:
@@ -648,8 +656,7 @@ class FittingView(d.Display):
slotMap = {}
# test for too many modules (happens with t3s / CCP change in slot layout)
for slotType in Slot.getTypes():
slot = Slot.getValue(slotType)
for slot in [e.value for e in FittingSlot]:
slotMap[slot] = fit.getSlotsFree(slot) < 0
for i, mod in enumerate(self.mods):
@@ -735,8 +742,8 @@ class FittingView(d.Display):
return
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
padding = 2

View File

@@ -150,7 +150,7 @@ class CharacterEntityEditor(EntityEditor):
class CharacterEditor(wx.Frame):
def __init__(self, parent):
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"))
self.SetIcon(i)
@@ -353,7 +353,7 @@ class SkillTreeView(wx.Panel):
self.skillBookDirtyImageId = self.imageList.Add(wx.Icon(BitmapLoader.getBitmap("skill_small_red", "gui")))
tree.AppendColumn("Skill")
tree.AppendColumn("Level")
tree.AppendColumn("Level", align=wx.ALIGN_CENTER)
# tree.SetMainColumn(0)
self.root = tree.GetRootItem()
@@ -361,13 +361,17 @@ class SkillTreeView(wx.Panel):
#
# 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.Bind(wx.EVT_BUTTON, self.onSecStatus)
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_CONTEXT_MENU, self.scheduleMenu)
@@ -427,7 +431,8 @@ class SkillTreeView(wx.Panel):
self.levelChangeMenu.Bind(wx.EVT_MENU, self.changeLevel)
self.SetSizer(pmainSizer)
self.Layout()
# This cuases issues with GTK, see #1866
# self.Layout()
def importSkills(self, evt):
@@ -554,9 +559,18 @@ class SkillTreeView(wx.Panel):
if event:
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):
root = event.GetItem()
tree = self.skillTreeListCtrl
child = tree.GetFirstChild(root)
if tree.GetItemText(child) == "dummy":
tree.DeleteItem(child)

View File

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

View File

@@ -26,6 +26,10 @@ import wx
from service.port.eft import EFT_OPTIONS
from service.port.multibuy import MULTIBUY_OPTIONS
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):
@@ -39,6 +43,17 @@ class CopySelectDialog(wx.Dialog):
def __init__(self, parent):
wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title="Select a format", size=(-1, -1),
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)
self.copyFormats = OrderedDict((
@@ -87,8 +102,8 @@ class CopySelectDialog(wx.Dialog):
self.options[formatId][optId] = checkbox
if self.settings['options'].get(formatId, {}).get(optId, defaultFormatOptions.get(formatId, {}).get(optId)):
checkbox.SetValue(True)
bsizer.Add(checkbox, 1, wx.EXPAND | wx.TOP | wx.BOTTOM, 3)
mainSizer.Add(bsizer, 1, wx.EXPAND | wx.LEFT, 20)
bsizer.Add(checkbox, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 3)
mainSizer.Add(bsizer, 0, wx.EXPAND | wx.LEFT, 20)
buttonSizer = self.CreateButtonSizer(wx.OK | wx.CANCEL)
if buttonSizer:
@@ -99,6 +114,31 @@ class CopySelectDialog(wx.Dialog):
self.Fit()
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):
obj = event.GetEventObject()
formatName = obj.GetLabel()
@@ -119,3 +159,27 @@ class CopySelectDialog(wx.Dialog):
for formatId in self.options:
options[formatId] = {optId: ch.IsChecked() for optId, ch in self.options[formatId].items()}
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 .guiChangeImplantLocation import GuiChangeImplantLocation
from .guiImportMutatedModule import GuiImportMutatedModuleCommand
from .guiSetSpoolup import GuiSetSpoolup
from .guiRebaseItems import GuiRebaseItemsCommand

View File

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

View File

@@ -1,5 +1,6 @@
import wx
from eos.saveddata.module import Module, State
from eos.saveddata.module import Module
from eos.const import FittingModuleState
import eos.db
from logbook import Logger
pyfalog = Logger(__name__)
@@ -28,7 +29,7 @@ class FitAddProjectedEnvCommand(wx.Command):
# todo: thing to check for existing environmental effects
module.state = State.ONLINE
module.state = FittingModuleState.ONLINE
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
self.old_item = fit.projectedModules.makeRoom(module)

View File

@@ -1,7 +1,8 @@
import wx
import eos.db
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__)
@@ -27,9 +28,9 @@ class FitAddProjectedModuleCommand(wx.Command):
except ValueError:
return False
module.state = State.ACTIVE
module.state = FittingModuleState.ACTIVE
if not module.canHaveState(module.state, fit):
module.state = State.OFFLINE
module.state = FittingModuleState.OFFLINE
fit.projectedModules.append(module)
eos.db.commit()

View File

@@ -1,5 +1,6 @@
import wx
from eos.saveddata.module import Module, State
from eos.saveddata.module import Module
from eos.const import FittingModuleState
import eos.db
from eos.db.gamedata.queries import getDynamicItem
from logbook import Logger
@@ -62,8 +63,8 @@ class FitImportMutatedCommand(wx.Command):
module.owner = fit
numSlots = len(fit.modules)
fit.modules.append(module)
if module.isValidState(State.ACTIVE):
module.state = State.ACTIVE
if module.isValidState(FittingModuleState.ACTIVE):
module.state = FittingModuleState.ACTIVE
# todo: fix these
# 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
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
pyfalog = Logger(__name__)
@@ -72,19 +73,21 @@ class FitReplaceModuleCommand(wx.Command):
# Dummy it out in case the next bit fails
fit.modules.toDummy(self.position)
if self.module.fits(fit):
self.module.owner = fit
fit.modules.toModule(self.position, self.module)
if self.module.isValidState(State.ACTIVE):
self.module.state = State.ACTIVE
if not self.module.fits(fit):
self.Undo()
return False
if self.old_module and self.old_module.charge and self.module.isValidCharge(self.old_module.charge):
self.module.charge = self.old_module.charge
self.module.owner = fit
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
# self.checkStates(fit, m)
if self.old_module and self.old_module.charge and self.module.isValidCharge(self.old_module.charge):
self.module.charge = self.old_module.charge
# fit.fill()
eos.db.commit()
return True
return False
# Then, check states of all modules and change where needed. This will recalc if needed
# self.checkStates(fit, m)
# 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):
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.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor()

View File

@@ -8,7 +8,7 @@ from .calc.fitAddFighter import FitAddFighterCommand
class GuiAddFighterCommand(wx.Command):
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.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor()

View File

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

View File

@@ -21,7 +21,7 @@ class GuiCargoToModuleCommand(wx.Command):
"""
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.sFit = Fit.getInstance()
self.fitID = fitID

View File

@@ -17,7 +17,7 @@ class GuiFillWithModuleCommand(wx.Command):
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
"""
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.sFit = Fit.getInstance()
self.fitID = fitID

View File

@@ -14,7 +14,7 @@ pyfalog = Logger(__name__)
class GuiModuleToCargoCommand(wx.Command):
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.sFit = Fit.getInstance()
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):
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.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor()

View File

@@ -8,7 +8,7 @@ from .calc.fitRemoveDrone import FitRemoveDroneCommand
class GuiRemoveDroneCommand(wx.Command):
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.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor()

View File

@@ -8,7 +8,7 @@ from .calc.fitRemoveFighter import FitRemoveFighterCommand
class GuiRemoveFighterCommand(wx.Command):
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.sFit = Fit.getInstance()
self.fitID = fitID

View File

@@ -27,7 +27,7 @@ class GuiRemoveProjectedCommand(wx.Command):
}
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.sFit = Fit.getInstance()
self.internal_history = wx.CommandProcessor()

View File

@@ -8,7 +8,7 @@ from .calc.fitSetMode import FitSetModeCommand
class GuiSetModeCommand(wx.Command):
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.sFit = Fit.getInstance()
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
from eos.config import gamedata_date, gamedata_version
from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues
from eos.db.saveddata.queries import getFit as db_getFit
# import this to access override setting
from eos.modifiedAttributeDict import ModifiedAttributeDict
from gui import graphFrame
@@ -64,11 +63,12 @@ from gui.setEditor import ImplantSetEditorDlg
from gui.shipBrowser import ShipBrowser
from gui.statsPane import StatsPane
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.esi import Esi
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.update import Update
import gui.fitCommands as cmd
@@ -508,6 +508,8 @@ class MainFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.saveCharAs, id=menuBar.saveCharAsId)
# Save current character
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
self.Bind(wx.EVT_MENU, self.eveFittings, id=menuBar.eveFittingsId)
@@ -655,6 +657,23 @@ class MainFrame(wx.Frame):
sChr.revertCharacter(charID)
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):
selTab = self.additionsSelect.index(event.GetId())
@@ -688,30 +707,6 @@ class MainFrame(wx.Frame):
else:
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):
clipboard = fromClipboard()
activeFit = self.getActiveFit()
@@ -728,28 +723,8 @@ class MainFrame(wx.Frame):
self._openAfterImport(importData)
def exportToClipboard(self, event):
CopySelectDict = {CopySelectDialog.copyFormatEft: self.clipboardEft,
CopySelectDialog.copyFormatXml: self.clipboardXml,
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>.")
with CopySelectDialog(self) as dlg:
dlg.ShowModal()
def exportSkillsNeeded(self, event):
""" 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 logbook import Logger
# from service.crest import Crest
# from service.crest import CrestModes
pyfalog = Logger(__name__)
@@ -59,6 +57,7 @@ class MainMenuBar(wx.MenuBar):
self.importDatabaseDefaultsId = wx.NewId()
self.toggleIgnoreRestrictionID = wx.NewId()
self.devToolsId = wx.NewId()
self.optimizeFitPrice = wx.NewId()
# pheonix: evaluate if this is needed
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.AppendSeparator()
self.ignoreRestrictionItem = editMenu.Append(self.toggleIgnoreRestrictionID, "Ignore Fitting Restrictions")
editMenu.AppendSeparator()
editMenu.Append(self.optimizeFitPrice, "Optimize Fit Price")
# Character menu
windowMenu = wx.Menu()
@@ -134,8 +136,6 @@ class MainMenuBar(wx.MenuBar):
preferencesItem.SetBitmap(BitmapLoader.getBitmap("preferences_small", "gui"))
windowMenu.Append(preferencesItem)
# self.sEsi = Crest.getInstance()
# CREST Menu
esiMMenu = wx.Menu()
self.Append(esiMMenu, "EVE &SSO")

View File

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

View File

@@ -2,6 +2,7 @@ import threading
import time
# noinspection PyPackageRequirements
import wx
from service.const import PortEftOptions
from service.settings import HTMLExportSettings
from service.fit import Fit
from service.port import Port
@@ -208,8 +209,10 @@ class exportHtmlThread(threading.Thread):
if self.stopRunning:
return
try:
eftFit = Port.exportEft(getFit(fit[0]))
print(eftFit)
eftFit = Port.exportEft(getFit(fit[0]), options={
PortEftOptions.IMPLANTS: True,
PortEftOptions.MUTATIONS: True,
PortEftOptions.LOADED_CHARGES: True})
HTMLfit = (
' <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 compareAttrs(attrs1, attrs2, attrHig):
"""
Compares received attribute sets. Returns:
- 0 if sets are different
- 1 if sets are exactly the same
- 2 if first set is strictly better
- 3 if second set is strictly better
"""
def compareAttrs(attrs1, attrs2):
# Consider items as different if they have no attrs
if len(attrs1) == 0 and len(attrs2) == 0:
return False
if set(attrs1) != set(attrs2):
return 0
return False
if all(attrs1[aid] == attrs2[aid] for aid in attrs1):
return 1
if all(
(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
return True
return False
print('finding replacements')
skillReqAttribs = {
182: 277,
183: 278,
@@ -213,10 +198,17 @@ def main(db, json_path):
1290: 1288}
skillReqAttribsFlat = set(skillReqAttribs.keys()).union(skillReqAttribs.values())
# Get data on type groups
# Format: {type ID: group ID}
typesGroups = {}
for row in tables['evetypes']:
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
# Format: {type ID: {attribute ID: attribute value}}
typesNormalAttribs = {}
typesSkillAttribs = {}
for row in tables['dgmtypeattribs']:
@@ -226,15 +218,23 @@ def main(db, json_path):
typeSkillAttribs[row['attributeID']] = row['value']
# Ignore these attributes for comparison purposes
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
633, # metaLevel
1692 # metaGroupID
1692, # metaGroupID
1768 # typeColorScheme
):
continue
else:
typeNormalAttribs = typesNormalAttribs.setdefault(row['typeID'], {})
typeNormalAttribs[row['attributeID']] = row['value']
# Get data on skill requirements
# Format: {type ID: {skill type ID: skill level}}
typesSkillReqs = {}
for typeID, typeAttribs in typesSkillAttribs.items():
typeSkillAttribs = typesSkillAttribs.get(typeID, {})
@@ -248,46 +248,54 @@ def main(db, json_path):
except (KeyError, ValueError):
continue
typeSkillReqs[skillType] = skillLevel
# Get data on attribute highIsGood flag
attrHig = {}
for row in tables['dgmattribs']:
attrHig[row['attributeID']] = bool(row['highIsGood'])
# Format: {group ID: category ID}
groupCategories = {}
for row in tables['evegroups']:
groupCategories[row['groupID']] = row['categoryID']
# As EVE affects various types mostly depending on their group or skill requirements,
# 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 = {}
for row in tables['evetypes']:
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, {})
# Ignore stuff w/o attributes
# Ignore items w/o attributes
if not typeAttribs:
continue
# We need only skill types, not levels for keys
typeSkillreqs = frozenset(typesSkillReqs.get(typeID, {}))
typeGroup = typesGroups[typeID]
groupData = groupedData.setdefault((typeGroup, typeSkillreqs), [])
typeEffects = frozenset(typesEffects.get(typeID, ()))
groupData = groupedData.setdefault((typeGroup, typeSkillreqs, typeEffects), [])
groupData.append((typeID, typeAttribs))
same = {}
better = {}
# Now, go through composed groups and for every item within it find items which are
# the same and which are better
# Format: {type ID: set(type IDs)}
replacements = {}
# Now, go through composed groups and for every item within it
# find items which are the same
for groupData in groupedData.values():
for type1, type2 in itertools.combinations(groupData, 2):
comparisonResult = compareAttrs(type1[1], type2[1], attrHig)
# Equal
if comparisonResult == 1:
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])
if compareAttrs(type1[1], type2[1]):
replacements.setdefault(type1[0], set()).add(type2[0])
replacements.setdefault(type2[0], set()).add(type1[0])
# Put this data into types table so that normal process hooks it up
for row in tables['evetypes']:
typeID = 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, ())))
row['replacements'] = ','.join('{}'.format(tid) for tid in sorted(replacements.get(row['typeID'], ())))
data = {}

View File

@@ -37,7 +37,8 @@ from service.esi import Esi
from eos.saveddata.implant import Implant as es_Implant
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
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 eos.db
from eos.enum import Enum
from service.const import EsiLoginMethod, EsiSsoMode
from eos.saveddata.ssocharacter import SsoCharacter
from service.esiAccess import APIException, SsoMode
from service.esiAccess import APIException
import gui.globalEvents as GE
from gui.ssoLogin import SsoLogin, SsoLoginServer
from service.server import StoppableHTTPServer, AuthHandler
@@ -24,11 +24,6 @@ from requests import Session
pyfalog = Logger(__name__)
class LoginMethod(Enum):
SERVER = 0
MANUAL = 1
class Esi(EsiAccess):
_instance = None
@@ -107,8 +102,8 @@ class Esi(EsiAccess):
def login(self):
# 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:
dlg = gui.ssoLogin.SsoLoginServer(6461 if self.settings.get('ssoMode') == SsoMode.CUSTOM else 0)
if self.settings.get('ssoMode') == EsiSsoMode.CUSTOM or self.settings.get('loginMode') == EsiLoginMethod.SERVER:
dlg = gui.ssoLogin.SsoLoginServer(6461 if self.settings.get('ssoMode') == EsiSsoMode.CUSTOM else 0)
dlg.ShowModal()
else:
dlg = gui.ssoLogin.SsoLogin()
@@ -142,7 +137,7 @@ class Esi(EsiAccess):
def handleLogin(self, message):
# 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]
auth_response = json.loads(base64.b64decode(ssoInfo))
else:

View File

@@ -17,7 +17,7 @@ import config
import base64
import datetime
from eos.enum import Enum
from service.const import EsiSsoMode, EsiEndpoints
from service.settings import EsiSettings, NetworkSettings
from requests import Session
@@ -42,11 +42,6 @@ scopes = [
]
class SsoMode(Enum):
AUTO = 0
CUSTOM = 1
class APIException(Exception):
""" Exception for SSO related errors """
@@ -66,13 +61,6 @@ class APIException(Exception):
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):
def __init__(self):
self.settings = EsiSettings.getInstance()
@@ -89,7 +77,7 @@ class EsiAccess(object):
@property
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://www.pyfa.io"
@@ -110,20 +98,20 @@ class EsiAccess(object):
return '%s/oauth/token' % self.sso_url
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):
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):
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):
# @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):
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
def update_token(char, tokenResponse):
@@ -136,7 +124,7 @@ class EsiAccess(object):
def getLoginURI(self, redirect=None):
self.state = str(uuid.uuid4())
if self.settings.get("ssoMode") == SsoMode.AUTO:
if self.settings.get("ssoMode") == EsiSsoMode.AUTO:
args = {
'state': self.state,
'pyfa_version': config.version,
@@ -183,7 +171,7 @@ class EsiAccess(object):
'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
return {
'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.drone import Drone as es_Drone
from eos.saveddata.fighter import Fighter as es_Fighter
from eos.saveddata.fit import Fit as FitType, ImplantLocation
from eos.saveddata.module import Module as es_Module, State
from eos.const import ImplantLocation, FittingModuleState
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 service.character import Character
from service.damagePattern import DamagePattern
@@ -146,6 +147,11 @@ class Fit(FitDeprecated):
pyfalog.debug("Getting count of all fits.")
return eos.db.countAllFits()
@staticmethod
def countAllFitsGroupedByShip():
count = eos.db.countFitGroupedByShip()
return count
@staticmethod
def countFitsWithShip(stuff):
pyfalog.debug("Getting count of all fits for: {0}", stuff)
@@ -347,7 +353,7 @@ class Fit(FitDeprecated):
elif isinstance(thing, es_Module):
thing.state = es_Module.getProposedState(thing, click)
if not thing.canHaveState(thing.state, fit):
thing.state = State.OFFLINE
thing.state = FittingModuleState.OFFLINE
elif isinstance(thing, FitType):
projectionInfo = thing.getProjectionInfo(fitID)
if projectionInfo:
@@ -379,8 +385,8 @@ class Fit(FitDeprecated):
if m.fits(fit):
m.owner = fit
fit.modules.toModule(position, m)
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
if m.isValidState(FittingModuleState.ACTIVE):
m.state = FittingModuleState.ACTIVE
# As some items may affect state-limiting attributes of the ship, calculate new attributes first
self.recalc(fit)
@@ -534,13 +540,13 @@ class Fit(FitDeprecated):
if mod != base:
# 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):
mod.state = State.ONLINE
mod.state = FittingModuleState.ONLINE
changed = True
for mod in fit.projectedModules:
# 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):
mod.state = State.OFFLINE
mod.state = FittingModuleState.OFFLINE
changed = True
for drone in fit.projectedDrones:
@@ -549,9 +555,26 @@ class Fit(FitDeprecated):
changed = True
return changed
# If any state was changed, recalculate attributes again
# if changed:
# self.recalc(fit)
@classmethod
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):
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.fighter import Fighter as es_Fighter
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 utils.deprecated import deprecated
@@ -304,16 +305,16 @@ class FitDeprecated(object):
fit.projectedFighters.append(fighter)
elif thing.group.name in es_Module.SYSTEM_GROUPS:
module = es_Module(thing)
module.state = State.ONLINE
module.state = FittingModuleState.ONLINE
fit.projectedModules.append(module)
else:
try:
module = es_Module(thing)
except ValueError:
return False
module.state = State.ACTIVE
module.state = FittingModuleState.ACTIVE
if not module.canHaveState(module.state, fit):
module.state = State.OFFLINE
module.state = FittingModuleState.OFFLINE
fit.projectedModules.append(module)
eos.db.commit()
@@ -396,8 +397,8 @@ class FitDeprecated(object):
m.owner = fit
numSlots = len(fit.modules)
fit.modules.append(m)
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
if m.isValidState(FittingModuleState.ACTIVE):
m.state = FittingModuleState.ACTIVE
# As some items may affect state-limiting attributes of the ship, calculate new attributes first
self.recalc(fit)
@@ -465,8 +466,8 @@ class FitDeprecated(object):
if m.fits(fit):
m.owner = fit
fit.modules.toModule(position, m)
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
if m.isValidState(FittingModuleState.ACTIVE):
m.state = FittingModuleState.ACTIVE
if recalc:
# As some items may affect state-limiting attributes of the ship, calculate new attributes first
@@ -508,8 +509,8 @@ class FitDeprecated(object):
try:
cargoP = es_Module(cargo.item)
cargoP.owner = fit
if cargoP.isValidState(State.ACTIVE):
cargoP.state = State.ACTIVE
if cargoP.isValidState(FittingModuleState.ACTIVE):
cargoP.state = FittingModuleState.ACTIVE
except:
pyfalog.warning("Invalid item: {0}", cargo.item)
return

View File

@@ -795,3 +795,20 @@ class Market(object):
"""Filter items by meta lvl"""
filtered = set([item for item in items if self.getMetaGroupIdByItem(item) in metas])
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/>.
# =============================================================================
import time
from xml.dom import minidom
from logbook import Logger
from eos.saveddata.price import PriceStatus
from service.network import Network
from service.price import Price, TIMEOUT, VALIDITY
from service.price import Price
pyfalog = Logger(__name__)
class EveMarketData(object):
class EveMarketData:
name = "eve-marketdata.com"
def __init__(self, types, system, priceMap):
data = {}
baseurl = "https://eve-marketdata.com/api/item_prices.xml"
data["system_id"] = system # Use Jita for market
data["type_ids"] = ','.join(str(x) for x in types)
def __init__(self, priceMap, system, fetchTimeout):
# 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 = {"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()
data = network.request(baseurl, network.PRICES, params=data)
data = network.request(baseurl, network.PRICES, params=params, timeout=fetchTimeout)
xml = minidom.parseString(data.text)
types = xml.getElementsByTagName("eve").item(0).getElementsByTagName("price")
@@ -55,19 +62,10 @@ class EveMarketData(object):
pyfalog.warning("Failed to get price for: {0}", type_)
continue
# Fill price data
priceobj = priceMap[typeID]
# eve-marketdata returns 0 if price data doesn't even exist for the item. In this case, don't reset the
# 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
# eve-marketdata returns 0 if price data doesn't even exist for the item
if price == 0:
continue
priceMap[typeID].update(PriceStatus.fetchSuccess, price)
del priceMap[typeID]

View File

@@ -17,33 +17,37 @@
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import time
from xml.dom import minidom
from logbook import Logger
from eos.saveddata.price import PriceStatus
from service.network import Network
from service.price import Price, VALIDITY
from service.price import Price
pyfalog = Logger(__name__)
class EveMarketer(object):
class EveMarketer:
name = "evemarketer"
def __init__(self, types, system, priceMap):
data = {}
def __init__(self, priceMap, system, fetchTimeout):
# 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"
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()
data = network.request(baseurl, network.PRICES, params=data)
data = network.request(baseurl, network.PRICES, params=params, timeout=fetchTimeout)
xml = minidom.parseString(data.text)
types = xml.getElementsByTagName("marketstat").item(0).getElementsByTagName("type")
# 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)
except (TypeError, ValueError):
pyfalog.warning("Failed to get price for: {0}", type_)
percprice = 0
continue
# Fill price data
priceobj = priceMap[typeID]
priceobj.price = percprice
priceobj.time = time.time() + VALIDITY
priceobj.status = PriceStatus.success
# Price is 0 if evemarketer has info on this item, but it is not available
# for current scope limit. If we provided scope limit - make sure to skip
# such items to check globally, and do not skip if requested globally
if percprice == 0 and system is not None:
continue
# delete price from working dict
priceMap[typeID].update(PriceStatus.fetchSuccess, percprice)
del priceMap[typeID]

View File

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

View File

@@ -28,8 +28,9 @@ from eos.saveddata.citadel import Citadel
from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
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.const import FittingSlot, FittingModuleState
from service.fit import Fit as svcFit
from service.market import Market
@@ -106,8 +107,8 @@ def importDna(string):
f.modules.append(m)
else:
m.owner = f
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
if m.isValidState(FittingModuleState.ACTIVE):
m.state = FittingModuleState.ACTIVE
moduleList.append(m)
# Recalc to get slot numbers correct for T3 cruisers
@@ -116,14 +117,14 @@ def importDna(string):
for module in moduleList:
if module.fits(f):
module.owner = f
if module.isValidState(State.ACTIVE):
module.state = State.ACTIVE
if module.isValidState(FittingModuleState.ACTIVE):
module.state = FittingModuleState.ACTIVE
f.modules.append(module)
return f
def exportDna(fit):
def exportDna(fit, callback):
dna = str(fit.shipID)
subsystems = [] # EVE cares which order you put these in
mods = OrderedDict()
@@ -131,7 +132,7 @@ def exportDna(fit):
sFit = svcFit.getInstance()
for mod in fit.modules:
if not mod.isEmpty:
if mod.slot == Slot.SUBSYSTEM:
if mod.slot == FittingSlot.SUBSYSTEM:
subsystems.append(mod)
continue
if mod.itemID not in mods:
@@ -173,4 +174,9 @@ def exportDna(fit):
for charge in charges:
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 service.fit import Fit
from service.market import Market
from eos.enum import Enum
from eos.saveddata.module import Hardpoint, Slot, Module, State
from eos.const import FittingModuleState, FittingHardpoint, FittingSlot
from service.const import PortEftRigSize
from eos.saveddata.module import Module
from eos.saveddata.drone import Drone
from eos.effectHandlerHelpers import HandledList
from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup
@@ -19,14 +20,6 @@ from logbook import Logger
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:
wepTestSet = {}
version = 0.03
@@ -58,13 +51,13 @@ class EfsPort:
mwd50mn = mapPropData("50MN Microwarpdrive II")
mwd500mn = mapPropData("500MN 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
elif rigSize == RigSize.MEDIUM:
elif rigSize == PortEftRigSize.MEDIUM:
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"]
elif rigSize == RigSize.CAPITAL:
elif rigSize == PortEftRigSize.CAPITAL:
propID = mwd50000mn["id"] if shipPower > mwd50000mn["powerReq"] else mwd500mn["id"]
if propID is None:
@@ -86,7 +79,7 @@ class EfsPort:
propWithBloom = next(filter(activePropWBloomFilter, propMods), None)
if propWithBloom is not None:
oldPropState = propWithBloom.state
propWithBloom.state = State.ONLINE
propWithBloom.state = FittingModuleState.ONLINE
sFit.recalc(fit)
sp = fit.maxSpeed
sig = fit.ship.getModifiedItemAttr("signatureRadius")
@@ -198,8 +191,8 @@ class EfsPort:
def getModuleInfo(fit, padTypeIDs=False):
moduleNames = []
modTypeIDs = []
moduleNameSets = {Slot.LOW: [], Slot.MED: [], Slot.HIGH: [], Slot.RIG: [], Slot.SUBSYSTEM: []}
modTypeIDSets = {Slot.LOW: [], Slot.MED: [], Slot.HIGH: [], Slot.RIG: [], Slot.SUBSYSTEM: []}
moduleNameSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []}
modTypeIDSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []}
for mod in fit.modules:
try:
if mod.item is not None:
@@ -216,17 +209,17 @@ class EfsPort:
pyfalog.error("Could not find name for module {0}".format(vars(mod)))
for modInfo in [
["High Slots:"], moduleNameSets[Slot.HIGH], ["", "Med Slots:"], moduleNameSets[Slot.MED],
["", "Low Slots:"], moduleNameSets[Slot.LOW], ["", "Rig Slots:"], moduleNameSets[Slot.RIG]
["High Slots:"], moduleNameSets[FittingSlot.HIGH], ["", "Med Slots:"], moduleNameSets[FittingSlot.MED],
["", "Low Slots:"], moduleNameSets[FittingSlot.LOW], ["", "Rig Slots:"], moduleNameSets[FittingSlot.RIG]
]:
moduleNames.extend(modInfo)
if len(moduleNameSets[Slot.SUBSYSTEM]) > 0:
if len(moduleNameSets[FittingSlot.SUBSYSTEM]) > 0:
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]:
if slotType is not Slot.SUBSYSTEM or len(modTypeIDSets[slotType]) > 0:
modTypeIDs.extend([0, 0] if slotType is not Slot.HIGH else [0])
for slotType in [FittingSlot.HIGH, FittingSlot.MED, FittingSlot.LOW, FittingSlot.RIG, FittingSlot.SUBSYSTEM]:
if slotType is not FittingSlot.SUBSYSTEM or len(modTypeIDSets[slotType]) > 0:
modTypeIDs.extend([0, 0] if slotType is not FittingSlot.HIGH else [0])
modTypeIDs.extend(modTypeIDSets[slotType])
droneNames = []
@@ -331,18 +324,18 @@ class EfsPort:
name = stats.item.name + ", " + stats.charge.name
else:
name = stats.item.name
if stats.hardpoint == Hardpoint.TURRET:
if stats.hardpoint == FittingHardpoint.TURRET:
tracking = stats.getModifiedItemAttr("trackingSpeed")
typeing = "Turret"
# 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")
explosionDelay = stats.getModifiedChargeAttr("explosionDelay")
damageReductionFactor = stats.getModifiedChargeAttr("aoeDamageReductionFactor")
explosionRadius = stats.getModifiedChargeAttr("aoeCloudSize")
explosionVelocity = stats.getModifiedChargeAttr("aoeVelocity")
typeing = "Missile"
elif stats.hardpoint == Hardpoint.NONE:
elif stats.hardpoint == FittingHardpoint.NONE:
aoeFieldRange = stats.getModifiedItemAttr("empFieldRange")
# This also covers non-bomb weapons with dps values and no hardpoints, most notably targeted doomsdays.
typeing = "SmartBomb"
@@ -496,11 +489,11 @@ class EfsPort:
getDroneMulti = lambda d: sumDamage(d.getModifiedItemAttr) * d.getModifiedItemAttr("damageMultiplier")
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
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
fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf)))
return fitMultipliers
@@ -538,7 +531,7 @@ class EfsPort:
if effect._Effect__effectModule is not None:
effect.handler(tf, fit.mode, [])
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 effect in sub.item.effects.values():
if effect._Effect__effectModule is not None:
@@ -583,7 +576,7 @@ class EfsPort:
return sizeNotFoundMsg
@staticmethod
def exportEfs(fit, typeNotFitFlag):
def exportEfs(fit, typeNotFitFlag, callback):
sFit = Fit.getInstance()
includeShipTypeData = typeNotFitFlag > 0
if includeShipTypeData:
@@ -680,4 +673,8 @@ class EfsPort:
pyfalog.error(e)
dataDict = {"name": fitName + "Fit could not be correctly parsed"}
export = json.dumps(dataDict, skipkeys=True)
return export
if callback:
callback(export)
else:
return export

View File

@@ -19,7 +19,6 @@
import re
from enum import Enum
from logbook import Logger
@@ -30,9 +29,11 @@ from eos.saveddata.booster import Booster
from eos.saveddata.drone import Drone
from eos.saveddata.fighter import Fighter
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.fit import Fit
from eos.const import FittingSlot, FittingModuleState
from service.const import PortEftOptions, PortEftRigSize
from service.fit import Fit as svcFit
from service.market import Market
from service.port.muta import parseMutant, renderMutant
@@ -41,26 +42,19 @@ from service.port.shared import IPortUser, fetchItem, processing_notify
pyfalog = Logger(__name__)
class Options(Enum):
IMPLANTS = 1
MUTATIONS = 2
LOADED_CHARGES = 3
EFT_OPTIONS = (
(Options.LOADED_CHARGES.value, 'Loaded Charges', 'Export charges loaded into modules', True),
(Options.MUTATIONS.value, 'Mutated Attributes', 'Export mutated modules\' stats', True),
(Options.IMPLANTS.value, 'Implants && Boosters', 'Export implants and boosters', True),
(PortEftOptions.LOADED_CHARGES, 'Loaded Charges', 'Export charges loaded into modules', True),
(PortEftOptions.MUTATIONS, 'Mutated Attributes', 'Export mutated modules\' stats', True),
(PortEftOptions.IMPLANTS, 'Implants && Boosters', 'Export implants and boosters', True),
)
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'
def exportEft(fit, options):
def exportEft(fit, options, callback):
# EFT formatted export is split in several sections, each section is
# separated from another using 2 blank lines. Sections might have several
# sub-sections, which are separated by 1 blank line
@@ -86,21 +80,21 @@ def exportEft(fit, options):
modName = module.baseItem.name
else:
modName = module.item.name
if module.isMutated and options[Options.MUTATIONS.value]:
if module.isMutated and options[PortEftOptions.MUTATIONS]:
mutants[mutantReference] = module
mutationSuffix = ' [{}]'.format(mutantReference)
mutantReference += 1
else:
mutationSuffix = ''
modOfflineSuffix = ' {}'.format(OFFLINE_SUFFIX) if module.state == State.OFFLINE else ''
if module.charge and options[Options.LOADED_CHARGES.value]:
modOfflineSuffix = ' {}'.format(OFFLINE_SUFFIX) if module.state == FittingModuleState.OFFLINE else ''
if module.charge and options[PortEftOptions.LOADED_CHARGES]:
rackLines.append('{}, {}{}{}'.format(
modName, module.charge.name, modOfflineSuffix, mutationSuffix))
else:
rackLines.append('{}{}{}'.format(modName, modOfflineSuffix, mutationSuffix))
else:
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:
modSection.append('\n'.join(rackLines))
if modSection:
@@ -122,15 +116,15 @@ def exportEft(fit, options):
sections.append('\n\n'.join(minionSection))
# Section 3: implants, boosters
if options[Options.IMPLANTS.value]:
if options[PortEftOptions.IMPLANTS]:
charSection = []
implantLines = []
for implant in fit.implants:
for implant in sorted(fit.implants, key=lambda i: i.slot or 0):
implantLines.append(implant.item.name)
if implantLines:
charSection.append('\n'.join(implantLines))
boosterLines = []
for booster in fit.boosters:
for booster in sorted(fit.boosters, key=lambda b: b.slot or 0):
boosterLines.append(booster.item.name)
if boosterLines:
charSection.append('\n'.join(boosterLines))
@@ -149,14 +143,19 @@ def exportEft(fit, options):
# Section 5: mutated modules' details
mutationLines = []
if mutants and options[Options.MUTATIONS.value]:
if mutants and options[PortEftOptions.MUTATIONS]:
for mutantReference in sorted(mutants):
mutant = mutants[mutantReference]
mutationLines.append(renderMutant(mutant, firstPrefix='[{}] '.format(mutantReference), prefix=' '))
if 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):
@@ -441,8 +440,8 @@ def importEftCfg(shipname, lines, iportuser):
else:
m.owner = fitobj
# Activate mod if it is activable
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
if m.isValidState(FittingModuleState.ACTIVE):
m.state = FittingModuleState.ACTIVE
# Add charge to mod if applicable, on any errors just don't add anything
if chargeName:
try:
@@ -722,12 +721,12 @@ class AbstractFit:
@property
def __slotContainerMap(self):
return {
Slot.HIGH: self.modulesHigh,
Slot.MED: self.modulesMed,
Slot.LOW: self.modulesLow,
Slot.RIG: self.rigs,
Slot.SUBSYSTEM: self.subsystems,
Slot.SERVICE: self.services}
FittingSlot.HIGH: self.modulesHigh,
FittingSlot.MED: self.modulesMed,
FittingSlot.LOW: self.modulesLow,
FittingSlot.RIG: self.rigs,
FittingSlot.SUBSYSTEM: self.subsystems,
FittingSlot.SERVICE: self.services}
def getContainerBySlot(self, slotType):
return self.__slotContainerMap.get(slotType)
@@ -798,10 +797,10 @@ class AbstractFit:
if itemSpec.charge is not None and m.isValidCharge(itemSpec.charge):
m.charge = itemSpec.charge
if itemSpec.offline and m.isValidState(State.OFFLINE):
m.state = State.OFFLINE
elif m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
if itemSpec.offline and m.isValidState(FittingModuleState.OFFLINE):
m.state = FittingModuleState.OFFLINE
elif m.isValidState(FittingModuleState.ACTIVE):
m.state = FittingModuleState.ACTIVE
return m
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.fighter import Fighter
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 service.fit import Fit as svcFit
from service.market import Market
@@ -41,12 +42,12 @@ class ESIExportException(Exception):
pyfalog = Logger(__name__)
INV_FLAGS = {
Slot.LOW: 11,
Slot.MED: 19,
Slot.HIGH: 27,
Slot.RIG: 92,
Slot.SUBSYSTEM: 125,
Slot.SERVICE: 164
FittingSlot.LOW: 11,
FittingSlot.MED: 19,
FittingSlot.HIGH: 27,
FittingSlot.RIG: 92,
FittingSlot.SUBSYSTEM: 125,
FittingSlot.SERVICE: 164
}
INV_FLAG_CARGOBAY = 5
@@ -54,7 +55,7 @@ INV_FLAG_DRONEBAY = 87
INV_FLAG_FIGHTER = 158
def exportESI(ofit):
def exportESI(ofit, callback):
# A few notes:
# max fit name length is 50 characters
# Most keys are created simply because they are required, but bogus data is okay
@@ -82,7 +83,7 @@ def exportESI(ofit):
item = nested_dict()
slot = module.slot
if slot == Slot.SUBSYSTEM:
if slot == FittingSlot.SUBSYSTEM:
# Order of subsystem matters based on this attr. See GH issue #130
slot = int(module.getModifiedItemAttr("subSystemSlot"))
item['flag'] = slot
@@ -134,7 +135,12 @@ def exportESI(ofit):
if len(fit['items']) == 0:
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):
@@ -189,8 +195,8 @@ def importESI(string):
if m.fits(fitobj):
fitobj.modules.append(m)
else:
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
if m.isValidState(FittingModuleState.ACTIVE):
m.state = FittingModuleState.ACTIVE
moduleList.append(m)

View File

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

View File

@@ -29,7 +29,7 @@ from bs4 import UnicodeDammit
from logbook import Logger
from eos import db
from eos.saveddata.fit import ImplantLocation
from eos.const import ImplantLocation
from service.fit import Fit as svcFit
from service.port.dna import exportDna, importDna
from service.port.eft import exportEft, importEft, importEftCfg
@@ -257,8 +257,8 @@ class Port(object):
return importEftCfg(shipname, lines, iportuser)
@classmethod
def exportEft(cls, fit, options):
return exportEft(fit, options)
def exportEft(cls, fit, options, callback=None):
return exportEft(fit, options, callback=callback)
# DNA-related methods
@staticmethod
@@ -266,8 +266,8 @@ class Port(object):
return importDna(string)
@staticmethod
def exportDna(fit):
return exportDna(fit)
def exportDna(fit, callback=None):
return exportDna(fit, callback=callback)
# ESI-related methods
@staticmethod
@@ -275,8 +275,8 @@ class Port(object):
return importESI(string)
@staticmethod
def exportESI(fit):
return exportESI(fit)
def exportESI(fit, callback=None):
return exportESI(fit, callback=callback)
# XML-related methods
@staticmethod
@@ -284,10 +284,10 @@ class Port(object):
return importXml(text, iportuser)
@staticmethod
def exportXml(iportuser=None, *fits):
return exportXml(iportuser, *fits)
def exportXml(iportuser=None, callback=None, *fits):
return exportXml(iportuser, callback=callback, *fits)
# Multibuy-related methods
@staticmethod
def exportMultiBuy(fit, options):
return exportMultiBuy(fit, options)
def exportMultiBuy(fit, options, callback=None):
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.fighter import Fighter
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.const import FittingSlot, FittingModuleState
from service.fit import Fit as svcFit
from service.market import Market
from utils.strfunctions import sequential_rep, replace_ltgt
@@ -198,8 +199,8 @@ def importXml(text, iportuser):
m.owner = fitobj
fitobj.modules.append(m)
else:
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
if m.isValidState(FittingModuleState.ACTIVE):
m.state = FittingModuleState.ACTIVE
moduleList.append(m)
@@ -225,14 +226,13 @@ def importXml(text, iportuser):
return fit_list
def exportXml(iportuser, *fits):
def exportXml(iportuser, callback, *fits):
doc = xml.dom.minidom.Document()
fittings = doc.createElement("fittings")
# fit count
fit_count = len(fits)
fittings.setAttribute("count", "%s" % fit_count)
doc.appendChild(fittings)
sFit = svcFit.getInstance()
for i, fit in enumerate(fits):
try:
@@ -266,7 +266,7 @@ def exportXml(iportuser, *fits):
slot = module.slot
if slot == Slot.SUBSYSTEM:
if slot == FittingSlot.SUBSYSTEM:
# Order of subsystem matters based on this attr. See GH issue #130
slotId = module.getModifiedItemAttr("subSystemSlot") - 125
else:
@@ -278,7 +278,7 @@ def exportXml(iportuser, *fits):
hardware = doc.createElement("hardware")
hardware.setAttribute("type", module.item.name)
slotName = Slot.getName(slot).lower()
slotName = FittingSlot(slot).name.lower()
slotName = slotName if slotName != "high" else "hi"
hardware.setAttribute("slot", "%s slot %d" % (slotName, slotId))
fitting.appendChild(hardware)
@@ -323,4 +323,9 @@ def exportXml(iportuser, *fits):
iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE,
(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 threading
import time
from itertools import chain
import wx
from logbook import Logger
from eos import db
from eos.saveddata.price import PriceStatus
from gui.fitCommands.guiRebaseItems import GuiRebaseItemsCommand
from service.fit import Fit
from service.market import Market
from service.network import TimeoutError
pyfalog = Logger(__name__)
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
class Price(object):
class Price:
instance = None
systemsList = {
@@ -69,38 +67,34 @@ class Price(object):
return cls.instance
@classmethod
def fetchPrices(cls, prices):
def fetchPrices(cls, prices, fetchTimeout, validityOverride):
"""Fetch all prices passed to this method"""
# Dictionary for our price objects
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:
if not price.isValid:
if not price.isValid(validityOverride):
priceMap[price.typeID] = price
if len(priceMap) == 0:
if not priceMap:
return
# Set of items which are still to be requested from this service
toRequest = set()
# Compose list of items we're going to request
for typeID in tuple(priceMap):
# Get item object
item = db.getItem(typeID)
# We're not going to request items only with market group, as eve-central
# doesn't provide any data for items not on the market
# We're not going to request items only with market group, as our current market
# sources do not provide any data for items not on the market
if item is None:
continue
if not item.marketGroupID:
priceMap[typeID].status = PriceStatus.notSupported
priceMap[typeID].update(PriceStatus.notSupported)
del priceMap[typeID]
continue
toRequest.add(typeID)
# Do not waste our time if all items are not on the market
if len(toRequest) == 0:
if not priceMap:
return
sFit = Fit.getInstance()
@@ -110,62 +104,46 @@ class Price(object):
return
# attempt to find user's selected price source, otherwise get first one
sourcesToTry = list(cls.sources.keys())
curr = sFit.serviceFittingOptions["priceSource"] if sFit.serviceFittingOptions["priceSource"] in sourcesToTry else sourcesToTry[0]
sourceAll = list(cls.sources.keys())
sourcePrimary = sFit.serviceFittingOptions["priceSource"] if sFit.serviceFittingOptions["priceSource"] in sourceAll else sourceAll[0]
while len(sourcesToTry) > 0:
sourcesToTry.remove(curr)
# Format: {source name: timeout weight}
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:
sourceCls = cls.sources.get(curr)
sourceCls(toRequest, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], priceMap)
break
# If getting or processing data returned any errors
sourceCls = cls.sources.get(source)
sourceCls(priceMap, cls.systemsList[sFit.serviceFittingOptions["priceSystem"]], sourceFetchTimeout)
except TimeoutError:
# Timeout error deserves special treatment
pyfalog.warning("Price fetch timout")
for typeID in tuple(priceMap):
priceobj = priceMap[typeID]
priceobj.time = time.time() + TIMEOUT
priceobj.status = PriceStatus.fail
del priceMap[typeID]
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]
pyfalog.warning("Price fetch timeout for source {}".format(source))
timedOutSources[source] = True
except Exception as e:
pyfalog.warn('Failed to fetch prices from price source {}: {}'.format(source, e))
# Sources remove price map items as they fetch info, if none remain then we're done
if not priceMap:
break
# if we get to this point, then we've got an error in all of our sources. Set to REREQUEST delay
for typeID in priceMap.keys():
priceobj = priceMap[typeID]
priceobj.time = time.time() + REREQUEST
priceobj.status = PriceStatus.fail
@classmethod
def fitItemsList(cls, fit):
# Compose a list of all the data we need & request it
fit_items = [fit.ship.item]
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))
# If we get to this point, then we've failed to get price with all our sources
# If all sources failed due to timeouts, set one status
if all(to is True for to in timedOutSources.values()):
for typeID in priceMap.keys():
priceMap[typeID].update(PriceStatus.fetchTimeout)
# If some sources failed due to any other reason, then it's definitely not network
# timeout and we just set another status
else:
for typeID in priceMap.keys():
priceMap[typeID].update(PriceStatus.fetchFail)
def getPriceNow(self, objitem):
"""Get price for provided typeID"""
@@ -174,7 +152,7 @@ class Price(object):
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"""
requests = []
sMkt = Market.getInstance()
@@ -186,7 +164,7 @@ class Price(object):
try:
callback(requests)
except Exception as e:
pyfalog.critical("Callback failed.")
pyfalog.critical("Execution of callback from getPrices failed.")
pyfalog.critical(e)
db.commit()
@@ -194,12 +172,43 @@ class Price(object):
if waitforthread:
self.priceWorkerThread.setToWait(requests, cb)
else:
self.priceWorkerThread.trigger(requests, cb)
self.priceWorkerThread.trigger(requests, cb, fetchTimeout, validityOverride)
def clearPriceCache(self):
pyfalog.debug("Clearing Prices")
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):
@@ -214,11 +223,11 @@ class PriceWorkerThread(threading.Thread):
queue = self.queue
while True:
# Grab our data
callback, requests = queue.get()
callback, requests, fetchTimeout, validityOverride = queue.get()
# Grab prices, this is the time-consuming part
if len(requests) > 0:
Price.fetchPrices(requests)
Price.fetchPrices(requests, fetchTimeout, validityOverride)
wx.CallAfter(callback)
queue.task_done()
@@ -230,14 +239,13 @@ class PriceWorkerThread(threading.Thread):
for callback in callbacks:
wx.CallAfter(callback)
def trigger(self, prices, callbacks):
self.queue.put((callbacks, prices))
def trigger(self, prices, callbacks, fetchTimeout, validityOverride):
self.queue.put((callbacks, prices, fetchTimeout, validityOverride))
def setToWait(self, prices, callback):
for x in prices:
if x.typeID not in self.wait:
self.wait[x.typeID] = []
self.wait[x.typeID].append(callback)
for price in prices:
callbacks = self.wait.setdefault(price.typeID, [])
callbacks.append(callback)
# 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