diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index d6bf4ae0c..458aa4ff2 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -477,7 +477,7 @@ class FittingView(d.Display): return if getattr(mod2, "modPosition") is not None: - if clone and mod2.isEmpty: + if clone and mod2.isEmpty and mod1.getModifiedItemAttr("maxGroupFitted", 0) < 1.0: sFit.cloneModule(self.mainFrame.getActiveFit(), srcIdx, mod2.modPosition) else: sFit.swapModules(self.mainFrame.getActiveFit(), srcIdx, mod2.modPosition) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index a70bea814..b74c93b3f 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -29,19 +29,21 @@ class CopySelectDialog(wx.Dialog): copyFormatDna = 3 copyFormatEsi = 4 copyFormatMultiBuy = 5 + copyFormatEfs = 6 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) mainSizer = wx.BoxSizer(wx.VERTICAL) - copyFormats = ["EFT", "EFT (Implants)", "XML", "DNA", "ESI", "MultiBuy"] + copyFormats = ["EFT", "EFT (Implants)", "XML", "DNA", "ESI", "MultiBuy", "EFS"] copyFormatTooltips = {CopySelectDialog.copyFormatEft: "EFT text format", CopySelectDialog.copyFormatEftImps: "EFT text format", CopySelectDialog.copyFormatXml: "EVE native XML format", CopySelectDialog.copyFormatDna: "A one-line text format", CopySelectDialog.copyFormatEsi: "A JSON format used for EVE CREST", - CopySelectDialog.copyFormatMultiBuy: "MultiBuy text format"} + CopySelectDialog.copyFormatMultiBuy: "MultiBuy text format", + CopySelectDialog.copyFormatEfs: "JSON data format used by EFS"} selector = wx.RadioBox(self, wx.ID_ANY, label="Copy to the clipboard using:", choices=copyFormats, style=wx.RA_SPECIFY_ROWS) selector.Bind(wx.EVT_RADIOBOX, self.Selected) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 9b237663c..9bd02feee 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -77,6 +77,7 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.db.saveddata.loadDefaultDatabaseValues import DefaultDatabaseValues from eos.db.saveddata.queries import getFit as db_getFit from service.port import Port, IPortUser +from service.efsPort import EfsPort from service.settings import HTMLExportSettings from time import gmtime, strftime @@ -726,6 +727,10 @@ class MainFrame(wx.Frame): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportMultiBuy(fit)) + def clipboardEfs(self): + fit = db_getFit(self.getActiveFit()) + toClipboard(EfsPort.exportEfs(fit, 0)) + def importFromClipboard(self, event): clipboard = fromClipboard() try: @@ -741,7 +746,8 @@ class MainFrame(wx.Frame): CopySelectDialog.copyFormatXml: self.clipboardXml, CopySelectDialog.copyFormatDna: self.clipboardDna, CopySelectDialog.copyFormatEsi: self.clipboardEsi, - CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy} + CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy, + CopySelectDialog.copyFormatEfs: self.clipboardEfs} dlg = CopySelectDialog(self) dlg.ShowModal() selected = dlg.GetSelected() diff --git a/service/efsPort.py b/service/efsPort.py new file mode 100755 index 000000000..d1dd394d9 --- /dev/null +++ b/service/efsPort.py @@ -0,0 +1,640 @@ +import inspect +import os +import platform +import re +import sys +import traceback +import json +import eos.db + +from math import log +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.saveddata.drone import Drone +from eos.effectHandlerHelpers import HandledList +from eos.db import gamedata_session, getItemsByCategory, getCategory, getAttributeInfo, getGroup +from eos.gamedata import Category, Group, Item, Traits, Attribute, Effect, ItemEffect +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.01 + + @staticmethod + def attrDirectMap(values, target, source): + for val in values: + target[val] = source.getModifiedItemAttr(val) + + @staticmethod + def getT2MwdSpeed(fit, sFit): + fitID = fit.ID + propID = None + shipHasMedSlots = fit.ship.getModifiedItemAttr("medSlots") > 0 + shipPower = fit.ship.getModifiedItemAttr("powerOutput") + # Monitors have a 99% reduction to prop mod power requirements + if fit.ship.name == "Monitor": + shipPower *= 100 + rigSize = fit.ship.getModifiedItemAttr("rigSize") + if not shipHasMedSlots: + return None + + filterVal = Item.groupID == getGroup("Propulsion Module").ID + propMods = gamedata_session.query(Item).options().filter(filterVal).all() + mapPropData = lambda propName: \ + next(map(lambda propMod: {"id": propMod.typeID, "powerReq": propMod.attributes["power"].value}, + (filter(lambda mod: mod.name == propName, propMods)))) + mwd5mn = mapPropData("5MN Microwarpdrive II") + mwd50mn = mapPropData("50MN Microwarpdrive II") + mwd500mn = mapPropData("500MN Microwarpdrive II") + mwd50000mn = mapPropData("50000MN Microwarpdrive II") + if rigSize == RigSize.SMALL or rigSize is None: + propID = mwd5mn["id"] if shipPower > mwd5mn["powerReq"] else None + elif rigSize == RigSize.MEDIUM: + propID = mwd50mn["id"] if shipPower > mwd50mn["powerReq"] else mwd5mn["id"] + elif rigSize == RigSize.LARGE: + propID = mwd500mn["id"] if shipPower > mwd500mn["powerReq"] else mwd50mn["id"] + elif rigSize == RigSize.CAPITAL: + propID = mwd50000mn["id"] if shipPower > mwd50000mn["powerReq"] else mwd500mn["id"] + + if propID is None: + return None + sFit.appendModule(fitID, propID) + sFit.recalc(fit) + fit = eos.db.getFit(fitID) + mwdPropSpeed = fit.maxSpeed + mwdPosition = list(filter(lambda mod: mod.item and mod.item.ID == propID, fit.modules))[0].position + sFit.removeModule(fitID, mwdPosition) + sFit.recalc(fit) + fit = eos.db.getFit(fitID) + return mwdPropSpeed + + @staticmethod + def getPropData(fit, sFit): + fitID = fit.ID + propMods = filter(lambda mod: mod.item and mod.item.group.name == "Propulsion Module", fit.modules) + activePropWBloomFilter = lambda mod: mod.state > 0 and "signatureRadiusBonus" in mod.item.attributes + propWithBloom = next(filter(activePropWBloomFilter, propMods), None) + if propWithBloom is not None: + oldPropState = propWithBloom.state + propWithBloom.state = State.ONLINE + sFit.recalc(fit) + fit = eos.db.getFit(fitID) + sp = fit.maxSpeed + sig = fit.ship.getModifiedItemAttr("signatureRadius") + propWithBloom.state = oldPropState + sFit.recalc(fit) + fit = eos.db.getFit(fitID) + return {"usingMWD": True, "unpropedSpeed": sp, "unpropedSig": sig} + return { + "usingMWD": False, + "unpropedSpeed": fit.maxSpeed, + "unpropedSig": fit.ship.getModifiedItemAttr("signatureRadius") + } + + @staticmethod + def getOutgoingProjectionData(fit): + # This is a subset of module groups capable of projection and a superset of those currently used by efs + modGroupNames = [ + "Remote Shield Booster", "Warp Scrambler", "Stasis Web", "Remote Capacitor Transmitter", + "Energy Nosferatu", "Energy Neutralizer", "Burst Jammer", "ECM", "Sensor Dampener", + "Weapon Disruptor", "Remote Armor Repairer", "Target Painter", "Remote Hull Repairer", + "Burst Projectors", "Warp Disrupt Field Generator", "Armor Resistance Shift Hardener", + "Target Breaker", "Micro Jump Drive", "Ship Modifiers", "Stasis Grappler", + "Ancillary Remote Shield Booster", "Ancillary Remote Armor Repairer", + "Titan Phenomena Generator", "Non-Repeating Hardeners" + ] + projectedMods = list(filter(lambda mod: mod.item and mod.item.group.name in modGroupNames, fit.modules)) + projections = [] + for mod in projectedMods: + maxRangeDefault = 0 + falloffDefault = 0 + stats = {} + if mod.item.group.name in ["Stasis Web", "Stasis Grappler"]: + stats["type"] = "Stasis Web" + stats["optimal"] = mod.getModifiedItemAttr("maxRange") + EfsPort.attrDirectMap(["duration", "speedFactor"], stats, mod) + elif mod.item.group.name == "Weapon Disruptor": + stats["type"] = "Weapon Disruptor" + stats["optimal"] = mod.getModifiedItemAttr("maxRange") + stats["falloff"] = mod.getModifiedItemAttr("falloffEffectiveness") + EfsPort.attrDirectMap([ + "trackingSpeedBonus", "maxRangeBonus", "falloffBonus", "aoeCloudSizeBonus", + "aoeVelocityBonus", "missileVelocityBonus", "explosionDelayBonus" + ], stats, mod) + elif mod.item.group.name == "Energy Nosferatu": + stats["type"] = "Energy Nosferatu" + EfsPort.attrDirectMap(["powerTransferAmount", "energyNeutralizerSignatureResolution"], stats, mod) + elif mod.item.group.name == "Energy Neutralizer": + stats["type"] = "Energy Neutralizer" + EfsPort.attrDirectMap([ + "energyNeutralizerSignatureResolution", "entityCapacitorLevelModifierSmall", + "entityCapacitorLevelModifierMedium", "entityCapacitorLevelModifierLarge", + "energyNeutralizerAmount" + ], stats, mod) + elif mod.item.group.name in ["Remote Shield Booster", "Ancillary Remote Shield Booster"]: + stats["type"] = "Remote Shield Booster" + EfsPort.attrDirectMap(["shieldBonus"], stats, mod) + elif mod.item.group.name in ["Remote Armor Repairer", "Ancillary Remote Armor Repairer"]: + stats["type"] = "Remote Armor Repairer" + EfsPort.attrDirectMap(["armorDamageAmount"], stats, mod) + elif mod.item.group.name == "Warp Scrambler": + stats["type"] = "Warp Scrambler" + EfsPort.attrDirectMap(["activationBlockedStrenght", "warpScrambleStrength"], stats, mod) + elif mod.item.group.name == "Target Painter": + stats["type"] = "Target Painter" + EfsPort.attrDirectMap(["signatureRadiusBonus"], stats, mod) + elif mod.item.group.name == "Sensor Dampener": + stats["type"] = "Sensor Dampener" + EfsPort.attrDirectMap(["maxTargetRangeBonus", "scanResolutionBonus"], stats, mod) + elif mod.item.group.name == "ECM": + stats["type"] = "ECM" + EfsPort.attrDirectMap([ + "scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus", + "scanRadarStrengthBonus", "scanLadarStrengthBonus", + ], stats, mod) + elif mod.item.group.name == "Burst Jammer": + stats["type"] = "Burst Jammer" + maxRangeDefault = mod.getModifiedItemAttr("ecmBurstRange") + EfsPort.attrDirectMap([ + "scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus", + "scanRadarStrengthBonus", "scanLadarStrengthBonus", + ], stats, mod) + elif mod.item.group.name == "Micro Jump Drive": + stats["type"] = "Micro Jump Drive" + EfsPort.attrDirectMap(["moduleReactivationDelay"], stats, mod) + else: + pyfalog.error("Projected module {0} lacks efs export implementation".format(mod.item.name)) + if mod.getModifiedItemAttr("maxRange", None) is None: + pyfalog.error("Projected module {0} has no maxRange".format(mod.item.name)) + stats["optimal"] = mod.getModifiedItemAttr("maxRange", maxRangeDefault) + stats["falloff"] = mod.getModifiedItemAttr("falloffEffectiveness", falloffDefault) + EfsPort.attrDirectMap(["duration", "capacitorNeed"], stats, mod) + projections.append(stats) + return projections + + # Note that unless padTypeIDs is True all 0s will be removed from modTypeIDs in the return. + # They always are added initally for the sake of brevity, as this option may not be retained long term. + @staticmethod + 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: []} + for mod in fit.modules: + try: + if mod.item is not None: + if mod.charge is not None: + modTypeIDSets[mod.slot].append([mod.item.typeID, mod.charge.typeID]) + moduleNameSets[mod.slot].append(mod.item.name + ": " + mod.charge.name) + else: + modTypeIDSets[mod.slot].append(mod.item.typeID) + moduleNameSets[mod.slot].append(mod.item.name) + else: + modTypeIDSets[mod.slot].append(0) + moduleNameSets[mod.slot].append("Empty Slot") + except: + 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] + ]: + moduleNames.extend(modInfo) + if len(moduleNameSets[Slot.SUBSYSTEM]) > 0: + moduleNames.extend(["", "Subsystems:"]) + moduleNames.extend(moduleNameSets[Slot.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]) + modTypeIDs.extend(modTypeIDSets[slotType]) + + droneNames = [] + droneIDs = [] + fighterNames = [] + fighterIDs = [] + for drone in fit.drones: + if drone.amountActive > 0: + droneIDs.append(drone.item.typeID) + droneNames.append("%s x%s" % (drone.item.name, drone.amount)) + for fighter in fit.fighters: + if fighter.amountActive > 0: + fighterIDs.append(fighter.item.typeID) + fighterNames.append("%s x%s" % (fighter.item.name, fighter.amountActive)) + if len(droneNames) > 0: + modTypeIDs.extend([0, 0]) + modTypeIDs.extend(droneIDs) + moduleNames.extend(["", "Drones:"]) + moduleNames.extend(droneNames) + if len(fighterNames) > 0: + modTypeIDs.extend([0, 0]) + modTypeIDs.extend(fighterIDs) + moduleNames.extend(["", "Fighters:"]) + moduleNames.extend(fighterNames) + if len(fit.implants) > 0: + modTypeIDs.extend([0, 0]) + moduleNames.extend(["", "Implants:"]) + for implant in fit.implants: + modTypeIDs.append(implant.item.typeID) + moduleNames.append(implant.item.name) + if len(fit.boosters) > 0: + modTypeIDs.extend([0, 0]) + moduleNames.extend(["", "Boosters:"]) + for booster in fit.boosters: + modTypeIDs.append(booster.item.typeID) + moduleNames.append(booster.item.name) + if len(fit.commandFits) > 0: + modTypeIDs.extend([0, 0]) + moduleNames.extend(["", "Command Fits:"]) + for commandFit in fit.commandFits: + modTypeIDs.append(commandFit.ship.item.typeID) + moduleNames.append(commandFit.name) + if len(fit.projectedModules) > 0: + modTypeIDs.extend([0, 0]) + moduleNames.extend(["", "Projected Modules:"]) + for mod in fit.projectedModules: + modTypeIDs.append(mod.item.typeID) + moduleNames.append(mod.item.name) + + if fit.character.name != "All 5": + modTypeIDs.extend([0, 0, 0]) + moduleNames.extend(["", "Character:"]) + moduleNames.append(fit.character.name) + if padTypeIDs is not True: + modTypeIDsUnpadded = [mod for mod in modTypeIDs if mod != 0] + modTypeIDs = modTypeIDsUnpadded + return {"moduleNames": moduleNames, "modTypeIDs": modTypeIDs} + + @staticmethod + def getFighterAbilityData(fighterAttr, fighter, baseRef): + baseRefDam = baseRef + "Damage" + abilityName = "RegularAttack" if baseRef == "fighterAbilityAttackMissile" else "MissileAttack" + rangeSuffix = "RangeOptimal" if baseRef == "fighterAbilityAttackMissile" else "Range" + reductionRef = baseRef if baseRef == "fighterAbilityAttackMissile" else baseRefDam + damageReductionFactor = log(fighterAttr(reductionRef + "ReductionFactor")) / log(fighterAttr(reductionRef + "ReductionSensitivity")) + damTypes = ["EM", "Therm", "Exp", "Kin"] + abBaseDamage = sum(map(lambda damType: fighterAttr(baseRefDam + damType), damTypes)) + abDamage = abBaseDamage * fighterAttr(baseRefDam + "Multiplier") + return { + "name": abilityName, "volley": abDamage * fighter.amountActive, "explosionRadius": fighterAttr(baseRef + "ExplosionRadius"), + "explosionVelocity": fighterAttr(baseRef + "ExplosionVelocity"), "optimal": fighterAttr(baseRef + rangeSuffix), + "damageReductionFactor": damageReductionFactor, "rof": fighterAttr(baseRef + "Duration"), + } + + @staticmethod + def getWeaponSystemData(fit): + weaponSystems = [] + groups = {} + for mod in fit.modules: + if mod.dps > 0: + # Group weapon + ammo combinations that occur more than once + keystr = str(mod.itemID) + "-" + str(mod.chargeID) + if keystr in groups: + groups[keystr][1] += 1 + else: + groups[keystr] = [mod, 1] + for wepGroup in groups.values(): + stats = wepGroup[0] + n = wepGroup[1] + tracking = 0 + maxVelocity = 0 + explosionDelay = 0 + damageReductionFactor = 0 + explosionRadius = 0 + explosionVelocity = 0 + aoeFieldRange = 0 + if stats.hardpoint == Hardpoint.TURRET: + tracking = stats.getModifiedItemAttr("trackingSpeed") + typeing = "Turret" + name = stats.item.name + ", " + stats.charge.name + # Bombs share most attributes with missiles despite not needing the hardpoint + elif stats.hardpoint == Hardpoint.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" + name = stats.item.name + ", " + stats.charge.name + elif stats.hardpoint == Hardpoint.NONE: + aoeFieldRange = stats.getModifiedItemAttr("empFieldRange") + # This also covers non-bomb weapons with dps values and no hardpoints, most notably targeted doomsdays. + typeing = "SmartBomb" + name = stats.item.name + statDict = { + "dps": stats.dps * n, "capUse": stats.capUse * n, "falloff": stats.falloff, + "type": typeing, "name": name, "optimal": stats.maxRange, + "numCharges": stats.numCharges, "numShots": stats.numShots, "reloadTime": stats.reloadTime, + "cycleTime": stats.cycleTime, "volley": stats.volley * n, "tracking": tracking, + "maxVelocity": maxVelocity, "explosionDelay": explosionDelay, "damageReductionFactor": damageReductionFactor, + "explosionRadius": explosionRadius, "explosionVelocity": explosionVelocity, "aoeFieldRange": aoeFieldRange, + "damageMultiplierBonusMax": stats.getModifiedItemAttr("damageMultiplierBonusMax"), + "damageMultiplierBonusPerCycle": stats.getModifiedItemAttr("damageMultiplierBonusPerCycle") + } + weaponSystems.append(statDict) + for drone in fit.drones: + if drone.dps[0] > 0 and drone.amountActive > 0: + droneAttr = drone.getModifiedItemAttr + # Drones are using the old tracking formula for trackingSpeed. This updates it to match turrets. + newTracking = droneAttr("trackingSpeed") / (droneAttr("optimalSigRadius") / 40000) + statDict = { + "dps": drone.dps[0], "cycleTime": drone.cycleTime, "type": "Drone", + "optimal": drone.maxRange, "name": drone.item.name, "falloff": drone.falloff, + "maxSpeed": droneAttr("maxVelocity"), "tracking": newTracking, + "volley": drone.dps[1] + } + weaponSystems.append(statDict) + for fighter in fit.fighters: + if fighter.dps[0] > 0 and fighter.amountActive > 0: + fighterAttr = fighter.getModifiedItemAttr + abilities = [] + if "fighterAbilityAttackMissileDamageEM" in fighter.item.attributes.keys(): + baseRef = "fighterAbilityAttackMissile" + ability = EfsPort.getFighterAbilityData(fighterAttr, fighter, baseRef) + abilities.append(ability) + if "fighterAbilityMissilesDamageEM" in fighter.item.attributes.keys(): + baseRef = "fighterAbilityMissiles" + ability = EfsPort.getFighterAbilityData(fighterAttr, fighter, baseRef) + abilities.append(ability) + statDict = { + "dps": fighter.dps[0], "type": "Fighter", "name": fighter.item.name, + "maxSpeed": fighterAttr("maxVelocity"), "abilities": abilities, + "ehp": fighterAttr("shieldCapacity") / 0.8875 * fighter.amountActive, + "volley": fighter.dps[1], "signatureRadius": fighterAttr("signatureRadius") + } + weaponSystems.append(statDict) + return weaponSystems + + @staticmethod + def getTestSet(setType): + def getT2ItemsWhere(additionalFilter, mustBeOffensive=False, category="Module"): + # Used to obtain a smaller subset of items while still containing examples of each group. + T2_META_LEVEL = 5 + metaLevelAttrID = getAttributeInfo("metaLevel").attributeID + categoryID = getCategory(category).categoryID + result = gamedata_session.query(Item).join(ItemEffect, Group, Attribute).\ + filter( + additionalFilter, + Attribute.attributeID == metaLevelAttrID, + Attribute.value == T2_META_LEVEL, + Group.categoryID == categoryID, + ).all() + if mustBeOffensive: + result = filter(lambda t: t.offensive is True, result) + return list(result) + + def getChargeType(item, setType): + if setType == "turret": + return str(item.attributes["chargeGroup1"].value) + "-" + str(item.attributes["chargeSize"].value) + return str(item.attributes["chargeGroup1"].value) + + if setType in EfsPort.wepTestSet.keys(): + return EfsPort.wepTestSet[setType] + else: + EfsPort.wepTestSet[setType] = [] + modSet = EfsPort.wepTestSet[setType] + + if setType == "drone": + ilist = getT2ItemsWhere(True, True, "Drone") + for item in ilist: + drone = Drone(item) + drone.amount = 1 + drone.amountActive = 1 + drone.itemModifiedAttributes.parent = drone + modSet.append(drone) + return modSet + + turretFittedEffectID = gamedata_session.query(Effect).filter(Effect.name == "turretFitted").first().effectID + launcherFittedEffectID = gamedata_session.query(Effect).filter(Effect.name == "launcherFitted").first().effectID + if setType == "launcher": + effectFilter = ItemEffect.effectID == launcherFittedEffectID + reqOff = False + else: + effectFilter = ItemEffect.effectID == turretFittedEffectID + reqOff = True + ilist = getT2ItemsWhere(effectFilter, reqOff) + previousChargeTypes = [] + # Get modules from item list + for item in ilist: + chargeType = getChargeType(item, setType) + # Only add turrets if we don"t already have one with the same size and ammo type. + if setType == "launcher" or chargeType not in previousChargeTypes: + previousChargeTypes.append(chargeType) + mod = Module(item) + modSet.append(mod) + + sMkt = Market.getInstance() + # Due to typed missile damage bonuses we"ll need to add extra launchers to cover all four types. + additionalLaunchers = [] + for mod in modSet: + clist = list(gamedata_session.query(Item).options(). + filter(Item.groupID == mod.getModifiedItemAttr("chargeGroup1")).all()) + mods = [mod] + charges = [clist[0]] + if setType == "launcher": + # We don"t want variations of missiles we already have + prevCharges = list(sMkt.getVariationsByItems(charges)) + testCharges = [] + for charge in clist: + if charge not in prevCharges: + testCharges.append(charge) + prevCharges += sMkt.getVariationsByItems([charge]) + for c in testCharges: + charges.append(c) + additionalLauncher = Module(mod.item) + mods.append(additionalLauncher) + for i in range(len(mods)): + mods[i].charge = charges[i] + mods[i].reloadForce = True + mods[i].state = 2 + if setType == "launcher" and i > 0: + additionalLaunchers.append(mods[i]) + modSet += additionalLaunchers + return modSet + + @staticmethod + def getWeaponBonusMultipliers(fit): + def sumDamage(attr): + totalDamage = 0 + for damageType in ["emDamage", "thermalDamage", "kineticDamage", "explosiveDamage"]: + if attr(damageType) is not None: + totalDamage += attr(damageType) + return totalDamage + + def getCurrentMultipliers(tf): + fitMultipliers = {} + 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) + 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) + getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.cycleTime + fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf))) + return fitMultipliers + + multipliers = {"turret": 1, "launcher": 1, "droneBandwidth": 1} + drones = EfsPort.getTestSet("drone") + launchers = EfsPort.getTestSet("launcher") + turrets = EfsPort.getTestSet("turret") + for weaponTypeSet in [turrets, launchers, drones]: + for mod in weaponTypeSet: + mod.owner = fit + turrets = list(filter(lambda mod: mod.getModifiedItemAttr("damageMultiplier"), turrets)) + launchers = list(filter(lambda mod: sumDamage(mod.getModifiedChargeAttr), launchers)) + + # Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits. + # standin class used to prevent . notation causing issues when used as an arg + class standin(): + pass + tf = standin() + tf.modules = HandledList(turrets + launchers) + tf.character = fit.character + tf.ship = fit.ship + tf.drones = HandledList(drones) + tf.fighters = HandledList([]) + tf.boosters = HandledList([]) + tf.extraAttributes = fit.extraAttributes + tf.mode = fit.mode + preTraitMultipliers = getCurrentMultipliers(tf) + for effect in fit.ship.item.effects.values(): + if effect._Effect__effectModule is not None: + effect.handler(tf, tf.ship, []) + # Factor in mode effects for T3 Destroyers + if fit.mode is not None: + for effect in fit.mode.item.effects.values(): + 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)) + for sub in subSystems: + for effect in sub.item.effects.values(): + if effect._Effect__effectModule is not None: + effect.handler(tf, sub, []) + postTraitMultipliers = getCurrentMultipliers(tf) + getMaxRatio = lambda dictA, dictB, key: max(map(lambda a, b: b / a, dictA[key], dictB[key])) + multipliers["turret"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "turrets"), 6) + multipliers["launcher"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "launchers"), 6) + multipliers["droneBandwidth"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "drones"), 6) + Fit.getInstance().recalc(fit) + return multipliers + + @staticmethod + def getShipSize(groupID): + # Size groupings are somewhat arbitrary but allow for a more managable number of top level groupings in a tree structure. + frigateGroupNames = ["Frigate", "Shuttle", "Corvette", "Assault Frigate", "Covert Ops", "Interceptor", + "Stealth Bomber", "Electronic Attack Ship", "Expedition Frigate", "Logistics Frigate"] + destroyerGroupNames = ["Destroyer", "Interdictor", "Tactical Destroyer", "Command Destroyer"] + cruiserGroupNames = ["Cruiser", "Heavy Assault Cruiser", "Logistics", "Force Recon Ship", + "Heavy Interdiction Cruiser", "Combat Recon Ship", "Strategic Cruiser"] + bcGroupNames = ["Combat Battlecruiser", "Command Ship", "Attack Battlecruiser"] + bsGroupNames = ["Battleship", "Elite Battleship", "Black Ops", "Marauder"] + capitalGroupNames = ["Titan", "Dreadnought", "Freighter", "Carrier", "Supercarrier", + "Capital Industrial Ship", "Jump Freighter", "Force Auxiliary"] + indyGroupNames = ["Industrial", "Deep Space Transport", "Blockade Runner", + "Mining Barge", "Exhumer", "Industrial Command Ship"] + miscGroupNames = ["Capsule", "Prototype Exploration Ship"] + shipSizes = [ + {"name": "Frigate", "groupIDs": map(lambda s: getGroup(s).ID, frigateGroupNames)}, + {"name": "Destroyer", "groupIDs": map(lambda s: getGroup(s).ID, destroyerGroupNames)}, + {"name": "Cruiser", "groupIDs": map(lambda s: getGroup(s).ID, cruiserGroupNames)}, + {"name": "Battlecruiser", "groupIDs": map(lambda s: getGroup(s).ID, bcGroupNames)}, + {"name": "Battleship", "groupIDs": map(lambda s: getGroup(s).ID, bsGroupNames)}, + {"name": "Capital", "groupIDs": map(lambda s: getGroup(s).ID, capitalGroupNames)}, + {"name": "Industrial", "groupIDs": map(lambda s: getGroup(s).ID, indyGroupNames)}, + {"name": "Misc", "groupIDs": map(lambda s: getGroup(s).ID, miscGroupNames)} + ] + for size in shipSizes: + if groupID in size["groupIDs"]: + return size["name"] + sizeNotFoundMsg = "ShipSize not found for groupID: " + str(groupID) + return sizeNotFoundMsg + + @staticmethod + def exportEfs(fit, typeNotFitFlag): + sFit = Fit.getInstance() + includeShipTypeData = typeNotFitFlag > 0 + if includeShipTypeData: + fitName = fit.name + else: + fitName = fit.ship.name + ": " + fit.name + pyfalog.info("Creating Eve Fleet Simulator data for: " + fit.name) + fitModAttr = fit.ship.getModifiedItemAttr + propData = EfsPort.getPropData(fit, sFit) + mwdPropSpeed = fit.maxSpeed + if includeShipTypeData: + mwdPropSpeed = EfsPort.getT2MwdSpeed(fit, sFit) + projections = EfsPort.getOutgoingProjectionData(fit) + modInfo = EfsPort.getModuleInfo(fit) + moduleNames = modInfo["moduleNames"] + modTypeIDs = modInfo["modTypeIDs"] + weaponSystems = EfsPort.getWeaponSystemData(fit) + + turretSlots = fitModAttr("turretSlotsLeft") if fitModAttr("turretSlotsLeft") is not None else 0 + launcherSlots = fitModAttr("launcherSlotsLeft") if fitModAttr("launcherSlotsLeft") is not None else 0 + droneBandwidth = fitModAttr("droneBandwidth") if fitModAttr("droneBandwidth") is not None else 0 + weaponBonusMultipliers = EfsPort.getWeaponBonusMultipliers(fit) + effectiveTurretSlots = round(turretSlots * weaponBonusMultipliers["turret"], 2) + effectiveLauncherSlots = round(launcherSlots * weaponBonusMultipliers["launcher"], 2) + effectiveDroneBandwidth = round(droneBandwidth * weaponBonusMultipliers["droneBandwidth"], 2) + # Assume a T2 siege module for dreads + if fit.ship.item.group.name == "Dreadnought": + effectiveTurretSlots *= 9.4 + effectiveLauncherSlots *= 15 + hullResonance = { + "exp": fitModAttr("explosiveDamageResonance"), "kin": fitModAttr("kineticDamageResonance"), + "therm": fitModAttr("thermalDamageResonance"), "em": fitModAttr("emDamageResonance") + } + armorResonance = { + "exp": fitModAttr("armorExplosiveDamageResonance"), "kin": fitModAttr("armorKineticDamageResonance"), + "therm": fitModAttr("armorThermalDamageResonance"), "em": fitModAttr("armorEmDamageResonance") + } + shieldResonance = { + "exp": fitModAttr("shieldExplosiveDamageResonance"), "kin": fitModAttr("shieldKineticDamageResonance"), + "therm": fitModAttr("shieldThermalDamageResonance"), "em": fitModAttr("shieldEmDamageResonance") + } + resonance = {"hull": hullResonance, "armor": armorResonance, "shield": shieldResonance} + shipSize = EfsPort.getShipSize(fit.ship.item.groupID) + try: + dataDict = { + "name": fitName, "ehp": fit.ehp, "droneDPS": fit.droneDPS, + "droneVolley": fit.droneVolley, "hp": fit.hp, "maxTargets": fit.maxTargets, + "maxSpeed": fit.maxSpeed, "weaponVolley": fit.weaponVolley, "totalVolley": fit.totalVolley, + "maxTargetRange": fit.maxTargetRange, "scanStrength": fit.scanStrength, + "weaponDPS": fit.weaponDPS, "alignTime": fit.alignTime, "signatureRadius": fitModAttr("signatureRadius"), + "weapons": weaponSystems, "scanRes": fitModAttr("scanResolution"), + "capUsed": fit.capUsed, "capRecharge": fit.capRecharge, + "rigSlots": fitModAttr("rigSlots"), "lowSlots": fitModAttr("lowSlots"), + "midSlots": fitModAttr("medSlots"), "highSlots": fitModAttr("hiSlots"), + "turretSlots": fitModAttr("turretSlotsLeft"), "launcherSlots": fitModAttr("launcherSlotsLeft"), + "powerOutput": fitModAttr("powerOutput"), "cpuOutput": fitModAttr("cpuOutput"), + "rigSize": fitModAttr("rigSize"), "effectiveTurrets": effectiveTurretSlots, + "effectiveLaunchers": effectiveLauncherSlots, "effectiveDroneBandwidth": effectiveDroneBandwidth, + "resonance": resonance, "typeID": fit.shipID, "groupID": fit.ship.item.groupID, "shipSize": shipSize, + "droneControlRange": fitModAttr("droneControlRange"), "mass": fitModAttr("mass"), + "unpropedSpeed": propData["unpropedSpeed"], "unpropedSig": propData["unpropedSig"], + "usingMWD": propData["usingMWD"], "mwdPropSpeed": mwdPropSpeed, "projections": projections, + "modTypeIDs": modTypeIDs, "moduleNames": moduleNames, + "pyfaVersion": pyfaVersion, "efsExportVersion": EfsPort.version + } + except TypeError: + pyfalog.error("Error parsing fit:" + str(fit)) + pyfalog.error(TypeError) + dataDict = {"name": fitName + "Fit could not be correctly parsed"} + export = json.dumps(dataDict, skipkeys=True) + return export diff --git a/service/fit.py b/service/fit.py index 122de2fa6..d45219cad 100644 --- a/service/fit.py +++ b/service/fit.py @@ -187,11 +187,11 @@ class Fit(object): # error during the command loop refreshFits = set() for projection in list(fit.projectedOnto.values()): - if projection.victim_fit != fit and projection.victim_fit in eos.db.saveddata_session: # GH issue #359 + if projection.victim_fit and projection.victim_fit != fit and projection.victim_fit in eos.db.saveddata_session: # GH issue #359 refreshFits.add(projection.victim_fit) for booster in list(fit.boostedOnto.values()): - if booster.boosted_fit != fit and booster.boosted_fit in eos.db.saveddata_session: # GH issue #359 + if booster.boosted_fit and booster.boosted_fit != fit and booster.boosted_fit in eos.db.saveddata_session: # GH issue #359 refreshFits.add(booster.boosted_fit) eos.db.remove(fit) @@ -626,12 +626,11 @@ class Fit(object): def changeModule(self, fitID, position, newItemID): fit = eos.db.getFit(fitID) + module = fit.modules[position] # We're trying to add a charge to a slot, which won't work. Instead, try to add the charge to the module in that slot. - if self.isAmmo(newItemID): - module = fit.modules[position] - if not module.isEmpty: - self.setAmmo(fitID, newItemID, [module]) + if self.isAmmo(newItemID) and not module.isEmpty: + self.setAmmo(fitID, newItemID, [module]) return True pyfalog.debug("Changing position of module from position ({0}) for fit ID: {1}", position, fitID) @@ -640,14 +639,17 @@ class Fit(object): # Dummy it out in case the next bit fails fit.modules.toDummy(position) - + ret = None try: m = es_Module(item) except ValueError: pyfalog.warning("Invalid item: {0}", newItemID) return False - - if m.fits(fit): + if m.slot != module.slot: + fit.modules.toModule(position, module) + # Fits, but we selected wrong slot type, so don't want to overwrite because we will append on failure (none) + ret = None + elif m.fits(fit): m.owner = fit fit.modules.toModule(position, m) if m.isValidState(State.ACTIVE): @@ -661,9 +663,8 @@ class Fit(object): fit.fill() eos.db.commit() - return True - else: - return None + ret = True + return ret def moveCargoToModule(self, fitID, moduleIdx, cargoIdx, copyMod=False): """ diff --git a/service/market.py b/service/market.py index 895cb2e98..545b1a66f 100644 --- a/service/market.py +++ b/service/market.py @@ -277,15 +277,8 @@ class Market(object): # Dictionary of items with forced market group (service assumes they have no # market group assigned in db, otherwise they'll appear in both original and forced groups) self.ITEMS_FORCEDMARKETGROUP = { - "Advanced Cerebral Accelerator" : 977, # Implants & Boosters > Booster - "Civilian Damage Control" : 615, # Ship Equipment > Hull & Armor > Damage Controls - "Civilian EM Ward Field" : 1695, - # Ship Equipment > Shield > Shield Hardeners > EM Shield Hardeners - "Civilian Explosive Deflection Field" : 1694, - # Ship Equipment > Shield > Shield Hardeners > Explosive Shield Hardeners + "Advanced Cerebral Accelerator" : 2487, # Implants & Boosters > Booster > Cerebral Accelerators "Civilian Hobgoblin" : 837, # Drones > Combat Drones > Light Scout Drones - "Civilian Kinetic Deflection Field" : 1693, - # Ship Equipment > Shield > Shield Hardeners > Kinetic Shield Hardeners "Civilian Light Missile Launcher" : 640, # Ship Equipment > Turrets & Bays > Missile Launchers > Light Missile Launchers "Civilian Scourge Light Missile" : 920, @@ -293,8 +286,6 @@ class Market(object): "Civilian Small Remote Armor Repairer" : 1059, # Ship Equipment > Hull & Armor > Remote Armor Repairers > Small "Civilian Small Remote Shield Booster" : 603, # Ship Equipment > Shield > Remote Shield Boosters > Small - "Civilian Stasis Webifier" : 683, # Ship Equipment > Electronic Warfare > Stasis Webifiers - "Civilian Warp Disruptor" : 1935, # Ship Equipment > Electronic Warfare > Warp Disruptors "Hardwiring - Zainou 'Sharpshooter' ZMX10" : 1493, # Implants & Boosters > Implants > Skill Hardwiring > Missile Implants > Implant Slot 06 "Hardwiring - Zainou 'Sharpshooter' ZMX100" : 1493, @@ -307,11 +298,9 @@ class Market(object): # Implants & Boosters > Implants > Skill Hardwiring > Missile Implants > Implant Slot 06 "Hardwiring - Zainou 'Sharpshooter' ZMX1100": 1493, # Implants & Boosters > Implants > Skill Hardwiring > Missile Implants > Implant Slot 06 - "Nugoehuvi Synth Blue Pill Booster" : 977, # Implants & Boosters > Booster - "Prototype Cerebral Accelerator" : 977, # Implants & Boosters > Booster + "Prototype Cerebral Accelerator" : 2487, # Implants & Boosters > Booster > Cerebral Accelerators "Prototype Iris Probe Launcher" : 712, # Ship Equipment > Turrets & Bays > Scan Probe Launchers - "Shadow" : 1310, # Drones > Combat Drones > Fighter Bombers - "Standard Cerebral Accelerator" : 977, # Implants & Boosters > Booster + "Standard Cerebral Accelerator" : 2487, # Implants & Boosters > Booster > Cerebral Accelerators } self.ITEMS_FORCEDMARKETGROUP_R = self.__makeRevDict(self.ITEMS_FORCEDMARKETGROUP) @@ -538,7 +527,7 @@ class Market(object): categories = ['Drone', 'Fighter', 'Implant'] for item in items: - if item.category.ID == 20: # Implants and Boosters + if item.category.ID == 20 and item.group.ID != 303: # Implants not Boosters implant_remove_list = set() implant_remove_list.add("Low-Grade ") implant_remove_list.add("Low-grade ") @@ -552,15 +541,6 @@ class Market(object): implant_remove_list.add(" - Elite") implant_remove_list.add(" - Improved") implant_remove_list.add(" - Standard") - implant_remove_list.add("Copper ") - implant_remove_list.add("Gold ") - implant_remove_list.add("Silver ") - implant_remove_list.add("Advanced ") - implant_remove_list.add("Improved ") - implant_remove_list.add("Prototype ") - implant_remove_list.add("Standard ") - implant_remove_list.add("Strong ") - implant_remove_list.add("Synth ") for implant_prefix in ("-6", "-7", "-8", "-9", "-10"): for i in range(50): @@ -596,6 +576,16 @@ class Market(object): if trimmed_variations_list: variations_list = trimmed_variations_list + # If the items are boosters then filter variations to only include boosters for the same slot. + BOOSTER_GROUP_ID = 303 + if all(map(lambda i: i.group.ID == BOOSTER_GROUP_ID, items)) and len(items) > 0: + # 'boosterness' is the database's attribute name for Booster Slot + reqSlot = next(items.__iter__()).getAttribute('boosterness') + # If the item and it's variation both have a marketGroupID it should match for the variation to be considered valid. + marketGroupID = [next(filter(None, map(lambda i: i.marketGroupID, items)), None), None] + matchSlotAndMktGrpID = lambda v: v.getAttribute('boosterness') == reqSlot and v.marketGroupID in marketGroupID + variations_list = list(filter(matchSlotAndMktGrpID, variations_list)) + variations.update(variations_list) return variations @@ -657,6 +647,12 @@ class Market(object): def marketGroupHasTypesCheck(self, mg): """If market group has any items, return true""" if mg and mg.ID in self.ITEMS_FORCEDMARKETGROUP_R: + # This shouldn't occur normally but makes errors more mild when ITEMS_FORCEDMARKETGROUP is outdated. + if len(mg.children) > 0 and len(mg.items) == 0: + pyfalog.error(("Market group \"{0}\" contains no items and has children. " + "ITEMS_FORCEDMARKETGROUP is likely outdated and will need to be " + "updated for {1} to display correctly.").format(mg, self.ITEMS_FORCEDMARKETGROUP_R[mg.ID])) + return False return True elif len(mg.items) > 0: return True