diff --git a/config.py b/config.py index 750888659..7afe652f5 100644 --- a/config.py +++ b/config.py @@ -24,10 +24,10 @@ saveInRoot = False # Version data -version = "2.3.1" +version = "2.4.0" tag = "Stable" -expansionName = "YC120.7" -expansionVersion = "1.2" +expansionName = "YC120.8" +expansionVersion = "1.0" evemonMinVersion = "4081" minItemSearchLength = 3 diff --git a/eos/effects/boostershieldcapacitypenalty.py b/eos/effects/boostershieldcapacitypenalty.py index ade1336d0..9258da2c5 100644 --- a/eos/effects/boostershieldcapacitypenalty.py +++ b/eos/effects/boostershieldcapacitypenalty.py @@ -1,7 +1,7 @@ # boosterShieldCapacityPenalty # # Used by: -# Implants from group: Booster (12 of 66) +# Implants from group: Booster (12 of 65) type = "boosterSideEffect" # User-friendly name for the side effect diff --git a/eos/effects/covertopsandreconopscloakmoduledelaybonus.py b/eos/effects/covertopsandreconopscloakmoduledelaybonus.py index a808a2650..7ea8e593d 100644 --- a/eos/effects/covertopsandreconopscloakmoduledelaybonus.py +++ b/eos/effects/covertopsandreconopscloakmoduledelaybonus.py @@ -3,9 +3,9 @@ # Used by: # Ships from group: Black Ops (5 of 5) # Ships from group: Blockade Runner (4 of 4) -# Ships from group: Covert Ops (7 of 7) +# Ships from group: Covert Ops (8 of 8) # Ships from group: Expedition Frigate (2 of 2) -# Ships from group: Force Recon Ship (8 of 8) +# Ships from group: Force Recon Ship (9 of 9) # Ships from group: Stealth Bomber (5 of 5) # Ships named like: Stratios (2 of 2) # Subsystems named like: Defensive Covert Reconfiguration (4 of 4) diff --git a/eos/effects/covertopscloakcpupercentbonus1.py b/eos/effects/covertopscloakcpupercentbonus1.py index 703b37189..e34450131 100644 --- a/eos/effects/covertopscloakcpupercentbonus1.py +++ b/eos/effects/covertopscloakcpupercentbonus1.py @@ -1,7 +1,7 @@ # covertOpsCloakCpuPercentBonus1 # # Used by: -# Ships from group: Covert Ops (5 of 7) +# Ships from group: Covert Ops (6 of 8) type = "passive" runTime = "early" diff --git a/eos/effects/covertopswarpresistance.py b/eos/effects/covertopswarpresistance.py new file mode 100644 index 000000000..1ab7538c1 --- /dev/null +++ b/eos/effects/covertopswarpresistance.py @@ -0,0 +1,9 @@ +# covertOpsWarpResistance +# +# Used by: +# Ships from group: Covert Ops (5 of 8) +type = "passive" + + +def handler(fit, src, context): + fit.ship.increaseItemAttr("warpFactor", src.getModifiedItemAttr("eliteBonusCovertOps1"), skill="Covert Ops") diff --git a/eos/effects/cynosuraldurationbonus.py b/eos/effects/cynosuraldurationbonus.py index cb306dded..4f78ea09e 100644 --- a/eos/effects/cynosuraldurationbonus.py +++ b/eos/effects/cynosuraldurationbonus.py @@ -1,7 +1,7 @@ # cynosuralDurationBonus # # Used by: -# Ships from group: Force Recon Ship (7 of 8) +# Ships from group: Force Recon Ship (8 of 9) type = "passive" diff --git a/eos/effects/cynosuraltheoryconsumptionbonus.py b/eos/effects/cynosuraltheoryconsumptionbonus.py index 217374cee..d67c969f8 100644 --- a/eos/effects/cynosuraltheoryconsumptionbonus.py +++ b/eos/effects/cynosuraltheoryconsumptionbonus.py @@ -1,7 +1,7 @@ # cynosuralTheoryConsumptionBonus # # Used by: -# Ships from group: Force Recon Ship (7 of 8) +# Ships from group: Force Recon Ship (8 of 9) # Skill: Cynosural Field Theory type = "passive" diff --git a/eos/effects/elitebonuscoveropsscanprobestrength2.py b/eos/effects/elitebonuscoveropsscanprobestrength2.py index 8b922cee1..77feea510 100644 --- a/eos/effects/elitebonuscoveropsscanprobestrength2.py +++ b/eos/effects/elitebonuscoveropsscanprobestrength2.py @@ -1,7 +1,7 @@ # eliteBonusCoverOpsScanProbeStrength2 # # Used by: -# Ships from group: Covert Ops (7 of 7) +# Ships from group: Covert Ops (8 of 8) type = "passive" diff --git a/eos/effects/elitebonusmaxdmgmultibonusadd.py b/eos/effects/elitebonusmaxdmgmultibonusadd.py new file mode 100644 index 000000000..7e9a5cc29 --- /dev/null +++ b/eos/effects/elitebonusmaxdmgmultibonusadd.py @@ -0,0 +1,10 @@ +# eliteBonusMaxDmgMultiBonusAdd +# +# Used by: +# Ship: Hydra +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemIncrease(lambda mod: mod.item.requiresSkill("Small Precursor Weapon"), "damageMultiplierBonusMax", + src.getModifiedItemAttr("eliteBonusCovertOps3"), skill="Covert Ops") diff --git a/eos/effects/elitebonusreconmaxdmgmultimaxhpt.py b/eos/effects/elitebonusreconmaxdmgmultimaxhpt.py new file mode 100644 index 000000000..1350d5a93 --- /dev/null +++ b/eos/effects/elitebonusreconmaxdmgmultimaxhpt.py @@ -0,0 +1,10 @@ +# eliteBonusReconMaxDmgMultiMaxHPT +# +# Used by: +# Ship: Tiamat +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemIncrease(lambda mod: mod.item.requiresSkill("Medium Precursor Weapon"), "damageMultiplierBonusMax", + src.getModifiedItemAttr("eliteBonusReconShip3"), skill="Recon Ships") diff --git a/eos/effects/elitebonusreconscanprobestrength2.py b/eos/effects/elitebonusreconscanprobestrength2.py new file mode 100644 index 000000000..55db46631 --- /dev/null +++ b/eos/effects/elitebonusreconscanprobestrength2.py @@ -0,0 +1,10 @@ +# eliteBonusReconScanProbeStrength2 +# +# Used by: +# Ship: Tiamat +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Astrometrics"), "baseSensorStrength", + src.getModifiedItemAttr("eliteBonusReconShip2"), skill="Recon Ships") diff --git a/eos/effects/minigamevirusstrengthbonus.py b/eos/effects/minigamevirusstrengthbonus.py index 2023d0407..0e60325db 100644 --- a/eos/effects/minigamevirusstrengthbonus.py +++ b/eos/effects/minigamevirusstrengthbonus.py @@ -1,7 +1,7 @@ # minigameVirusStrengthBonus # # Used by: -# Ships from group: Covert Ops (7 of 7) +# Ships from group: Covert Ops (7 of 8) # Ships named like: Stratios (2 of 2) # Subsystems named like: Defensive Covert Reconfiguration (4 of 4) # Ship: Astero diff --git a/eos/effects/reconshipcloakcpubonus1.py b/eos/effects/reconshipcloakcpubonus1.py index 8f63a28f2..e7c05671c 100644 --- a/eos/effects/reconshipcloakcpubonus1.py +++ b/eos/effects/reconshipcloakcpubonus1.py @@ -1,7 +1,7 @@ # reconShipCloakCpuBonus1 # # Used by: -# Ships from group: Force Recon Ship (6 of 8) +# Ships from group: Force Recon Ship (7 of 9) type = "passive" runTime = "early" diff --git a/eos/effects/shipbonusneutcapneedrolebonus2.py b/eos/effects/shipbonusneutcapneedrolebonus2.py index 324a01c66..b48e784ad 100644 --- a/eos/effects/shipbonusneutcapneedrolebonus2.py +++ b/eos/effects/shipbonusneutcapneedrolebonus2.py @@ -2,7 +2,9 @@ # # Used by: # Ship: Damavik +# Ship: Hydra # Ship: Leshak +# Ship: Tiamat # Ship: Vedmak type = "passive" diff --git a/eos/effects/shipbonuspctdamagepc1.py b/eos/effects/shipbonuspctdamagepc1.py index 50db0c467..06e686ab2 100644 --- a/eos/effects/shipbonuspctdamagepc1.py +++ b/eos/effects/shipbonuspctdamagepc1.py @@ -1,6 +1,7 @@ # shipbonusPCTDamagePC1 # # Used by: +# Ship: Tiamat # Ship: Vedmak type = "passive" diff --git a/eos/effects/shipbonuspctdamagepf1.py b/eos/effects/shipbonuspctdamagepf1.py index 7fd636e18..59a1071e2 100644 --- a/eos/effects/shipbonuspctdamagepf1.py +++ b/eos/effects/shipbonuspctdamagepf1.py @@ -2,6 +2,7 @@ # # Used by: # Ship: Damavik +# Ship: Hydra type = "passive" diff --git a/eos/effects/shipbonuspctoptimalpf2.py b/eos/effects/shipbonuspctoptimalpf2.py index 424b0d282..7403b2146 100644 --- a/eos/effects/shipbonuspctoptimalpf2.py +++ b/eos/effects/shipbonuspctoptimalpf2.py @@ -2,6 +2,7 @@ # # Used by: # Ship: Damavik +# Ship: Hydra type = "passive" diff --git a/eos/effects/shipbonuspcttrackingpc2.py b/eos/effects/shipbonuspcttrackingpc2.py index 69224ac73..4914d290e 100644 --- a/eos/effects/shipbonuspcttrackingpc2.py +++ b/eos/effects/shipbonuspcttrackingpc2.py @@ -1,6 +1,7 @@ # shipbonusPCTTrackingPC2 # # Used by: +# Ship: Tiamat # Ship: Vedmak type = "passive" diff --git a/eos/effects/shipbonusremoterepcapneedrolebonus2.py b/eos/effects/shipbonusremoterepcapneedrolebonus2.py index d2b7f5ea7..64df7fa9e 100644 --- a/eos/effects/shipbonusremoterepcapneedrolebonus2.py +++ b/eos/effects/shipbonusremoterepcapneedrolebonus2.py @@ -2,7 +2,9 @@ # # Used by: # Ship: Damavik +# Ship: Hydra # Ship: Leshak +# Ship: Tiamat # Ship: Vedmak type = "passive" diff --git a/eos/effects/shipbonusremoterepmaxrangerolebonus1.py b/eos/effects/shipbonusremoterepmaxrangerolebonus1.py index cdcc2773d..fc2fc3b14 100644 --- a/eos/effects/shipbonusremoterepmaxrangerolebonus1.py +++ b/eos/effects/shipbonusremoterepmaxrangerolebonus1.py @@ -2,7 +2,9 @@ # # Used by: # Ship: Damavik +# Ship: Hydra # Ship: Leshak +# Ship: Tiamat # Ship: Vedmak type = "passive" diff --git a/eos/effects/shipbonussmartbombcapneedrolebonus2.py b/eos/effects/shipbonussmartbombcapneedrolebonus2.py index 611e9822b..07eb85b54 100644 --- a/eos/effects/shipbonussmartbombcapneedrolebonus2.py +++ b/eos/effects/shipbonussmartbombcapneedrolebonus2.py @@ -2,7 +2,9 @@ # # Used by: # Ship: Damavik +# Ship: Hydra # Ship: Leshak +# Ship: Tiamat # Ship: Vedmak type = "passive" diff --git a/eos/effects/shipbonussurveyprobeexplosiondelayskillsurveycovertops3.py b/eos/effects/shipbonussurveyprobeexplosiondelayskillsurveycovertops3.py index db215459d..4fc7124c5 100644 --- a/eos/effects/shipbonussurveyprobeexplosiondelayskillsurveycovertops3.py +++ b/eos/effects/shipbonussurveyprobeexplosiondelayskillsurveycovertops3.py @@ -1,7 +1,7 @@ # shipBonusSurveyProbeExplosionDelaySkillSurveyCovertOps3 # # Used by: -# Ships from group: Covert Ops (5 of 7) +# Ships from group: Covert Ops (5 of 8) type = "passive" diff --git a/eve.db b/eve.db index ecab30628..9727bd3fb 100644 Binary files a/eve.db and b/eve.db differ 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 5dd119db0..a4befdeda 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 @@ -734,6 +735,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: @@ -749,7 +754,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/imgs/icons/10160.png b/imgs/icons/10160.png new file mode 100644 index 000000000..ce1558e0b Binary files /dev/null and b/imgs/icons/10160.png differ diff --git a/imgs/icons/10851.png b/imgs/icons/10851.png new file mode 100644 index 000000000..30642ceb3 Binary files /dev/null and b/imgs/icons/10851.png differ diff --git a/imgs/icons/21785.png b/imgs/icons/21785.png new file mode 100644 index 000000000..d52e7c71d Binary files /dev/null and b/imgs/icons/21785.png differ diff --git a/imgs/icons/22029.png b/imgs/icons/22029.png new file mode 100644 index 000000000..a5e2f7eca Binary files /dev/null and b/imgs/icons/22029.png differ diff --git a/imgs/icons/22030.png b/imgs/icons/22030.png new file mode 100644 index 000000000..627468bb9 Binary files /dev/null and b/imgs/icons/22030.png differ diff --git a/imgs/icons/22031.png b/imgs/icons/22031.png new file mode 100644 index 000000000..f9ffc890b Binary files /dev/null and b/imgs/icons/22031.png differ diff --git a/imgs/icons/22034.png b/imgs/icons/22034.png new file mode 100644 index 000000000..aa3e8941f Binary files /dev/null and b/imgs/icons/22034.png differ diff --git a/imgs/icons/22036.png b/imgs/icons/22036.png new file mode 100644 index 000000000..7de00e638 Binary files /dev/null and b/imgs/icons/22036.png differ diff --git a/imgs/icons/22041.png b/imgs/icons/22041.png new file mode 100644 index 000000000..560421e25 Binary files /dev/null and b/imgs/icons/22041.png differ diff --git a/imgs/renders/21344.png b/imgs/renders/21344.png new file mode 100644 index 000000000..f45f26972 Binary files /dev/null and b/imgs/renders/21344.png differ diff --git a/imgs/renders/21362.png b/imgs/renders/21362.png new file mode 100644 index 000000000..f6dff6ea2 Binary files /dev/null and b/imgs/renders/21362.png differ diff --git a/imgs/renders/21405.png b/imgs/renders/21405.png new file mode 100644 index 000000000..55a6cc3a5 Binary files /dev/null and b/imgs/renders/21405.png differ diff --git a/imgs/renders/21406.png b/imgs/renders/21406.png new file mode 100644 index 000000000..e9d8d8610 Binary files /dev/null and b/imgs/renders/21406.png differ diff --git a/scripts/icons_update.py b/scripts/icons_update.py index 7205e3e08..0ad4c5359 100644 --- a/scripts/icons_update.py +++ b/scripts/icons_update.py @@ -141,6 +141,7 @@ def get_icon_file(res_path, size): icon for it. Return as PIL image object down- scaled for use in pyfa. """ + res_path = res_path.replace('//', '/') #1703 if res_path not in res_index: return None res_icon = res_index[res_path] @@ -192,7 +193,10 @@ if toadd: print(('Adding {} icons...'.format(len(toadd)))) missing = set() for fname in sorted(toadd): - icon = icon_json[str(fname)] + icon = icon_json.get(str(fname), None) + if icon is None: + print("Can't find iconID {}".format(fname)) + continue key = icon['iconFile'].lower() icon = get_icon_file(key, ICON_SIZE) if icon is None: diff --git a/scripts/jsonToSql.py b/scripts/jsonToSql.py index 3cb2caf0a..2d1b3ed01 100755 --- a/scripts/jsonToSql.py +++ b/scripts/jsonToSql.py @@ -25,7 +25,7 @@ import re # Add eos root path to sys.path so we can import ourselves path = os.path.dirname(__file__) -sys.path.insert(0, os.path.realpath(os.path.join(path, ".."))) +sys.path.insert(0, os.path.realpath(os.path.join(path, '..'))) import json import argparse @@ -54,65 +54,65 @@ def main(db, json_path): # Config dict tables = { - "clonegrades": eos.gamedata.AlphaCloneSkill, - "dgmattribs": eos.gamedata.AttributeInfo, - "dgmeffects": eos.gamedata.Effect, - "dgmtypeattribs": eos.gamedata.Attribute, - "dgmtypeeffects": eos.gamedata.ItemEffect, - "dgmunits": eos.gamedata.Unit, - "evecategories": eos.gamedata.Category, - "evegroups": eos.gamedata.Group, - "invmetagroups": eos.gamedata.MetaGroup, - "invmetatypes": eos.gamedata.MetaType, - "evetypes": eos.gamedata.Item, - "phbtraits": eos.gamedata.Traits, - "phbmetadata": eos.gamedata.MetaData, - "mapbulk_marketGroups": eos.gamedata.MarketGroup, + 'clonegrades': eos.gamedata.AlphaCloneSkill, + 'dgmattribs': eos.gamedata.AttributeInfo, + 'dgmeffects': eos.gamedata.Effect, + 'dgmtypeattribs': eos.gamedata.Attribute, + 'dgmtypeeffects': eos.gamedata.ItemEffect, + 'dgmunits': eos.gamedata.Unit, + 'evecategories': eos.gamedata.Category, + 'evegroups': eos.gamedata.Group, + 'invmetagroups': eos.gamedata.MetaGroup, + 'invmetatypes': eos.gamedata.MetaType, + 'evetypes': eos.gamedata.Item, + 'phbtraits': eos.gamedata.Traits, + 'phbmetadata': eos.gamedata.MetaData, + 'mapbulk_marketGroups': eos.gamedata.MarketGroup, } fieldMapping = { - "dgmattribs": { - "displayName_en-us": "displayName" + 'dgmattribs': { + 'displayName_en-us': 'displayName' }, - "dgmeffects": { - "displayName_en-us": "displayName", - "description_en-us": "description" + 'dgmeffects': { + 'displayName_en-us': 'displayName', + 'description_en-us': 'description' }, - "dgmunits": { - "displayName_en-us": "displayName" + 'dgmunits': { + 'displayName_en-us': 'displayName' }, #icons??? - "evecategories": { - "categoryName_en-us": "categoryName" + 'evecategories': { + 'categoryName_en-us': 'categoryName' }, - "evegroups": { - "groupName_en-us": "groupName" + 'evegroups': { + 'groupName_en-us': 'groupName' }, - "invmetagroups": { - "metaGroupName_en-us": "metaGroupName" + 'invmetagroups': { + 'metaGroupName_en-us': 'metaGroupName' }, - "evetypes": { - "typeName_en-us": "typeName", - "description_en-us": "description" + 'evetypes': { + 'typeName_en-us': 'typeName', + 'description_en-us': 'description' }, #phbtraits??? - "mapbulk_marketGroups": { - "marketGroupName_en-us": "marketGroupName", - "description_en-us": "description" + 'mapbulk_marketGroups': { + 'marketGroupName_en-us': 'marketGroupName', + 'description_en-us': 'description' } } rowsInValues = ( - "evetypes", - "evegroups", - "evecategories" + 'evetypes', + 'evegroups', + 'evecategories' ) def convertIcons(data): new = [] for k, v in list(data.items()): - v["iconID"] = k + v['iconID'] = k new.append(v) return new @@ -126,23 +126,23 @@ def main(db, json_path): check = {} for ID in data: - for skill in data[ID]["skills"]: + for skill in data[ID]['skills']: newData.append({ - "alphaCloneID": int(ID), - "alphaCloneName": "Alpha Clone", - "typeID": skill["typeID"], - "level": skill["level"]}) + 'alphaCloneID': int(ID), + 'alphaCloneName': 'Alpha Clone', + 'typeID': skill['typeID'], + 'level': skill['level']}) if ID not in check: check[ID] = {} - check[ID][int(skill["typeID"])] = int(skill["level"]) + check[ID][int(skill['typeID'])] = int(skill['level']) if not functools.reduce(lambda a, b: a if a == b else False, [v for _, v in check.items()]): - raise Exception("Alpha Clones not all equal") + raise Exception('Alpha Clones not all equal') newData = [x for x in newData if x['alphaCloneID'] == 1] if len(newData) == 0: - raise Exception("Alpha Clone processing failed") + raise Exception('Alpha Clone processing failed') return newData @@ -150,28 +150,28 @@ def main(db, json_path): def convertSection(sectionData): sectionLines = [] - headerText = "{}".format(sectionData["header"]) + headerText = '{}'.format(sectionData['header']) sectionLines.append(headerText) - for bonusData in sectionData["bonuses"]: - prefix = "{} ".format(bonusData["number"]) if "number" in bonusData else "" - bonusText = "{}{}".format(prefix, bonusData["text"].replace("\u00B7", "\u2022 ")) + for bonusData in sectionData['bonuses']: + prefix = '{} '.format(bonusData['number']) if 'number' in bonusData else '' + bonusText = '{}{}'.format(prefix, bonusData['text'].replace('\u00B7', '\u2022 ')) sectionLines.append(bonusText) - sectionLine = "
\n".join(sectionLines) + sectionLine = '
\n'.join(sectionLines) return sectionLine newData = [] for row in data: typeLines = [] - typeId = row["typeID"] - traitData = row["traits"] - for skillData in sorted(traitData.get("skills", ()), key=lambda i: i["header"]): + typeId = row['typeID'] + traitData = row['traits_en-us'] + for skillData in sorted(traitData.get('skills', ()), key=lambda i: i['header']): typeLines.append(convertSection(skillData)) - if "role" in traitData: - typeLines.append(convertSection(traitData["role"])) - if "misc" in traitData: - typeLines.append(convertSection(traitData["misc"])) - traitLine = "
\n
\n".join(typeLines) - newRow = {"typeID": typeId, "traitText": traitLine} + if 'role' in traitData: + typeLines.append(convertSection(traitData['role'])) + if 'misc' in traitData: + typeLines.append(convertSection(traitData['misc'])) + traitLine = '
\n
\n'.join(typeLines) + newRow = {'typeID': typeId, 'traitText': traitLine} newData.append(newRow) return newData @@ -179,15 +179,15 @@ def main(db, json_path): # Dump all data to memory so we can easely cross check ignored rows for jsonName, cls in tables.items(): - with open(os.path.join(jsonPath, "{}.json".format(jsonName)), encoding="utf-8") as f: + with open(os.path.join(jsonPath, '{}.json'.format(jsonName)), encoding='utf-8') as f: tableData = json.load(f) if jsonName in rowsInValues: tableData = list(tableData.values()) - if jsonName == "icons": + if jsonName == 'icons': tableData = convertIcons(tableData) - if jsonName == "phbtraits": + if jsonName == 'phbtraits': tableData = convertTraits(tableData) - if jsonName == "clonegrades": + if jsonName == 'clonegrades': tableData = convertClones(tableData) data[jsonName] = tableData @@ -195,23 +195,23 @@ def main(db, json_path): # Sometimes CCP unpublishes some items we want to have published, we # can do it here - just add them to initial set eveTypes = set() - for row in data["evetypes"]: - if (row["published"] + for row in data['evetypes']: + if (row['published'] or row['groupID'] == 1306 # group Ship Modifiers, for items like tactical t3 ship modes - or row['typeName'].startswith('Civilian') # Civilian weapons + or row['typeName_en-us'].startswith('Civilian') # Civilian weapons or row['typeID'] in (41549, 41548, 41551, 41550) # Micro Bombs (Fighters) or row['groupID'] in ( 1882, 1975, 1971, - 1983 # the "container" for the abysmal environments - ) # Abysmal weather (environment) + 1983 # the "container" for the abyssal environments + ) # Abyssal weather (environment) ): - eveTypes.add(row["typeID"]) + eveTypes.add(row['typeID']) # ignore checker def isIgnored(file, row): - if file in ("evetypes", "dgmtypeeffects", "dgmtypeattribs", "invmetatypes") and row['typeID'] not in eveTypes: + if file in ('evetypes', 'dgmtypeeffects', 'dgmtypeattribs', 'invmetatypes') and row['typeID'] not in eveTypes: return True return False @@ -220,31 +220,31 @@ def main(db, json_path): fieldMap = fieldMapping.get(jsonName, {}) tmp = [] - print("processing {}".format(jsonName)) + print('processing {}'.format(jsonName)) for row in table: # We don't care about some kind of rows, filter it out if so if not isIgnored(jsonName, row): - if jsonName == 'evetypes' and row["typeName"].startswith('Civilian'): # Apparently people really want Civilian modules available - row["published"] = True + if jsonName == 'evetypes' and row['typeName_en-us'].startswith('Civilian'): # Apparently people really want Civilian modules available + row['published'] = True instance = tables[jsonName]() # fix for issue 80 - if jsonName is "icons" and "res:/ui/texture/icons/" in str(row["iconFile"]).lower(): - row["iconFile"] = row["iconFile"].lower().replace("res:/ui/texture/icons/", "").replace(".png", "") + if jsonName is 'icons' and 'res:/ui/texture/icons/' in str(row['iconFile']).lower(): + row['iconFile'] = row['iconFile'].lower().replace('res:/ui/texture/icons/', '').replace('.png', '') # with res:/ui... references, it points to the actual icon file (including it's size variation of #_size_#) # strip this info out and get the identifying info split = row['iconFile'].split('_') if len(split) == 3: - row['iconFile'] = "{}_{}".format(split[0], split[2]) - if jsonName is "icons" and "modules/" in str(row["iconFile"]).lower(): - row["iconFile"] = row["iconFile"].lower().replace("modules/", "").replace(".png", "") + row['iconFile'] = '{}_{}'.format(split[0], split[2]) + if jsonName is 'icons' and 'modules/' in str(row['iconFile']).lower(): + row['iconFile'] = row['iconFile'].lower().replace('modules/', '').replace('.png', '') - if jsonName is "clonegrades": - if (row["alphaCloneID"] not in tmp): + if jsonName is 'clonegrades': + if (row['alphaCloneID'] not in tmp): cloneParent = eos.gamedata.AlphaClone() - setattr(cloneParent, "alphaCloneID", row["alphaCloneID"]) - setattr(cloneParent, "alphaCloneName", row["alphaCloneName"]) + setattr(cloneParent, 'alphaCloneID', row['alphaCloneID']) + setattr(cloneParent, 'alphaCloneName', row['alphaCloneName']) eos.db.gamedata_session.add(cloneParent) tmp.append(row['alphaCloneID']) @@ -256,7 +256,7 @@ def main(db, json_path): eos.db.gamedata_session.add(instance) # quick and dirty hack to get this data in - with open(os.path.join(jsonPath, "dynamicAttributes.json"), encoding="utf-8") as f: + with open(os.path.join(jsonPath, 'dynamicAttributes.json'), encoding='utf-8') as f: bulkdata = json.load(f) for mutaID, data in bulkdata.items(): muta = eos.gamedata.DynamicItem() @@ -283,25 +283,25 @@ def main(db, json_path): # CCP still has 5 subsystems assigned to T3Cs, even though only 4 are available / usable. They probably have some # old legacy requirement or assumption that makes it difficult for them to change this value in the data. But for # pyfa, we can do it here as a post-processing step - eos.db.gamedata_engine.execute("UPDATE dgmtypeattribs SET value = 4.0 WHERE attributeID = ?", (1367,)) + eos.db.gamedata_engine.execute('UPDATE dgmtypeattribs SET value = 4.0 WHERE attributeID = ?', (1367,)) - eos.db.gamedata_engine.execute("UPDATE invtypes SET published = 0 WHERE typeName LIKE '%abyssal%'") + eos.db.gamedata_engine.execute('UPDATE invtypes SET published = 0 WHERE typeName LIKE \'%abyssal%\'') print() for x in CATEGORIES_TO_REMOVE: cat = eos.db.gamedata_session.query(eos.gamedata.Category).filter(eos.gamedata.Category.ID == x).first() - print ("Removing Category: {}".format(cat.name)) + print ('Removing Category: {}'.format(cat.name)) eos.db.gamedata_session.delete(cat) eos.db.gamedata_session.commit() - eos.db.gamedata_engine.execute("VACUUM") + eos.db.gamedata_engine.execute('VACUUM') - print("done") + print('done') -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="This scripts dumps effects from an sqlite cache dump to mongo") - parser.add_argument("-d", "--db", required=True, type=str, help="The sqlalchemy connectionstring, example: sqlite:///c:/tq.db") - parser.add_argument("-j", "--json", required=True, type=str, help="The path to the json dump") +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='This scripts dumps effects from an sqlite cache dump to mongo') + parser.add_argument('-d', '--db', required=True, type=str, help='The sqlalchemy connectionstring, example: sqlite:///c:/tq.db') + parser.add_argument('-j', '--json', required=True, type=str, help='The path to the json dump') args = parser.parse_args() main(args.db, args.json) 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 1f4b417dd..ae5a59eb8 100644 --- a/service/fit.py +++ b/service/fit.py @@ -208,11 +208,11 @@ class Fit(FitDeprecated): # 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) 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 diff --git a/service/port.py b/service/port.py index a0dd0a89c..e659a5899 100644 --- a/service/port.py +++ b/service/port.py @@ -306,8 +306,11 @@ class Port(object): fit.character = sFit.character fit.damagePattern = sFit.pattern fit.targetResists = sFit.targetResists - useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"] - fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT + if len(fit.implants) > 0: + fit.implantLocation = ImplantLocation.FIT + else: + useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"] + fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT db.save(fit) # IDs.append(fit.ID) if iportuser: # Pulse @@ -339,8 +342,11 @@ class Port(object): fit.character = sFit.character fit.damagePattern = sFit.pattern fit.targetResists = sFit.targetResists - useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"] - fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT + if len(fit.implants) > 0: + fit.implantLocation = ImplantLocation.FIT + else: + useCharImplants = sFit.serviceFittingOptions["useCharacterImplantsByDefault"] + fit.implantLocation = ImplantLocation.CHARACTER if useCharImplants else ImplantLocation.FIT db.save(fit) return fits