From 0de950862b89cc4d28580d100d65f6941ec46ac0 Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Sun, 11 Mar 2018 03:28:48 -0400 Subject: [PATCH 01/49] Added a crude data exporter for effs Known to be quite buggy and needs formating adjustments. In order to export fit data it first requires data to be exported with pyfas minimal html exporter. The resulting pyfaFits.html file should be placed in the project directory before running effs_stat_export.py. --- effs_stat_export.py | 474 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100755 effs_stat_export.py diff --git a/effs_stat_export.py b/effs_stat_export.py new file mode 100755 index 000000000..604cc250e --- /dev/null +++ b/effs_stat_export.py @@ -0,0 +1,474 @@ +import inspect +import os +import platform +import re +import sys +import traceback +from optparse import AmbiguousOptionError, BadOptionError, OptionParser + +from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, StreamHandler, TimedRotatingFileHandler, WARNING, \ + __version__ as logbook_version + +import config + +try: + import wxversion +except ImportError: + wxversion = None + +try: + import sqlalchemy +except ImportError: + sqlalchemy = None + +pyfalog = Logger(__name__) + +class PassThroughOptionParser(OptionParser): + + def _process_args(self, largs, rargs, values): + while rargs: + try: + OptionParser._process_args(self, largs, rargs, values) + except (BadOptionError, AmbiguousOptionError) as e: + pyfalog.error("Bad startup option passed.") + largs.append(e.opt_str) + +usage = "usage: %prog [--root]" +parser = PassThroughOptionParser(usage=usage) +parser.add_option("-r", "--root", action="store_true", dest="rootsavedata", help="if you want pyfa to store its data in root folder, use this option", default=False) +parser.add_option("-w", "--wx28", action="store_true", dest="force28", help="Force usage of wxPython 2.8", default=False) +parser.add_option("-d", "--debug", action="store_true", dest="debug", help="Set logger to debug level.", default=False) +parser.add_option("-t", "--title", action="store", dest="title", help="Set Window Title", default=None) +parser.add_option("-s", "--savepath", action="store", dest="savepath", help="Set the folder for savedata", default=None) +parser.add_option("-l", "--logginglevel", action="store", dest="logginglevel", help="Set desired logging level [Critical|Error|Warning|Info|Debug]", default="Error") + +(options, args) = parser.parse_args() + +if options.rootsavedata is True: + config.saveInRoot = True + +# set title if it wasn't supplied by argument +if options.title is None: + options.title = "pyfa %s%s - Python Fitting Assistant" % (config.version, "" if config.tag.lower() != 'git' else " (git)") + +config.debug = options.debug + +# convert to unicode if it is set +if options.savepath is not None: + options.savepath = unicode(options.savepath) +config.defPaths(options.savepath) + +try: + # noinspection PyPackageRequirements + import wx +except: + exit_message = "Cannot import wxPython. You can download wxPython (2.8+) from http://www.wxpython.org/" + raise PreCheckException(exit_message) + +try: + import requests + config.requestsVersion = requests.__version__ +except ImportError: + raise PreCheckException("Cannot import requests. You can download requests from https://pypi.python.org/pypi/requests.") + +import eos.db + +#if config.saVersion[0] > 0 or config.saVersion[1] >= 7: + # <0.7 doesn't have support for events ;_; (mac-deprecated) +config.sa_events = True +import eos.events + + # noinspection PyUnresolvedReferences +import service.prefetch # noqa: F401 + + # Make sure the saveddata db exists +if not os.path.exists(config.savePath): + os.mkdir(config.savePath) + +eos.db.saveddata_meta.create_all() + + +armorLinkShip = eos.db.searchFits('armor links')[0] +infoLinkShip = eos.db.searchFits('information links')[0] +shieldLinkShip = eos.db.searchFits('shield links')[0] +skirmishLinkShip = eos.db.searchFits('skirmish links')[0] +import json + +def processExportedHtml(fileLocation): + output = open('./jsonShipStatExport.js', 'w') + output.write('let shipJSON = JSON.stringify([') + outputBaseline = open('./jsonShipBaseStatExport.js', 'w') + outputBaseline.write('let shipBaseJSON = JSON.stringify([') + shipCata = eos.db.getItemsByCategory('Ship') + #shipCata = eos.db.getItem(638) + #shipCata = eos.db.getMetaGroup(638) + #shipCata = eos.db.getAttributeInfo(638) + #shipCata = eos.db.getItemsByCategory('Traits') + #shipCata = eos.db.getGroup('invtraits') + #shipCata = eos.db.getCategory('Traits') + from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, Table + from sqlalchemy.orm import relation, mapper, synonym, deferred + from eos.db import gamedata_session + from eos.db import gamedata_meta + from eos.db.gamedata.metaGroup import metatypes_table, items_table + from eos.db.gamedata.group import groups_table + + from eos.gamedata import AlphaClone, Attribute, Category, Group, Item, MarketGroup, \ + MetaGroup, AttributeInfo, MetaData, Effect, ItemEffect, Traits + from eos.db.gamedata.traits import traits_table + #shipCata = traits_table #categories_table + #print shipCata + #print shipCata.columns + #print shipCata.categoryName + #print vars(shipCata) + data = category = gamedata_session.query(Category).all() + #print data + #print iter(data) + eff = gamedata_session.query(Category).get(53) #Bonus (id14) #Effects (id 53) + data = eff; + #print eff + #print vars(eff) + things = []#[Category, MetaGroup, AttributeInfo, MetaData, Item, Attribute, Effect, ItemEffect, Traits]#, Attribute] + if False: + for dataTab in things : + print 'Data for: ' + str(dataTab) + try: + filter = dataTab.typeID == 638 + except: + filter = dataTab.ID == 638 + data = gamedata_session.query(dataTab).options().filter(filter).all() + print data + try: + varDict = vars(data) + print varDict + except: + print 'Not a Dict' + try: + varDict = data.__doc__ + print varDict + except: + print 'No items()' + try: + for varDict in data: + print varDict + print vars(varDict) + except: + print 'Not a list of dicts' + + #print vars(shipCata._sa_instance_state) + baseLimit = 500 + baseN = 0 + for ship in iter(shipCata): + if baseN < baseLimit: + #print ship + #print ship.ID + #print ship.categoryName + #print vars(ship) + dna = str(ship.ID) + stats = setFitFromString(dna, ship.name, ship.groupID) + outputBaseline.write(stats) + outputBaseline.write(',\n') + baseN += 1; + limit = 500 + skipTill = 0 + n = 0 + try: + with open('pyfaFits.html'): + fileLocation = 'pyfaFits.html' + except: + try: + with open('.pyfa/pyfaFits.html'): + fileLocation = '.pyfa/pyfaFits.html' + except: + try: + with open('../.pyfa/pyfaFits.html'): + fileLocation = '../.pyfa/pyfaFits.html' + except: + try: + with open('../../.pyfa/pyfaFits.html'): + fileLocation = '../../.pyfa/pyfaFits.html' + except: + fileLocation = None; + if fileLocation != None: + with open(fileLocation) as f: + for fullLine in f: + if limit == None or n < limit: + n += 1 + startInd = fullLine.find('/dna/') + 5 + line = fullLine[startInd:len(fullLine)] + endInd = line.find('::') + dna = line[0:endInd] + name = line[line.find('>') + 1:line.find('<')] + if n >= skipTill: + print 'name: ' + name + ' DNA: ' + dna + stats = setFitFromString(dna, name, 0) + output.write(stats) + output.write(',\n') + output.write(']);\nexport {shipJSON};') + output.close() + outputBaseline.write(']);\nexport {shipBaseJSON};') + outputBaseline.close() +def parseNeededFitDetails(fit, groupID): + singleRunPrintPreformed = False + weaponSystems = [] + groups = {} + #help(fit.modules) + #help(fit.modules[0]) + for mod in fit.modules: + if mod.dps > 0: + keystr = str(mod.itemID) + '-' + str(mod.chargeID) + if keystr in groups: + groups[keystr][1] += 1 + else: + groups[keystr] = [mod, 1] + for wepGroup in groups: + stats = groups[wepGroup][0] + c = groups[wepGroup][1] + tracking = 0 + maxVelocity = 0 + explosionDelay = 0 + damageReductionFactor = 0 + explosionRadius = 0 + explosionVelocity = 0 + aoeFieldRange = 0 + if stats.hardpoint == 2: + tracking = stats.itemModifiedAttributes['trackingSpeed'] + typeing = 'Turret' + name = stats.item.name + ', ' + stats.charge.name + elif stats.hardpoint == 1: + maxVelocity = stats.chargeModifiedAttributes['maxVelocity'] + explosionDelay = stats.chargeModifiedAttributes['explosionDelay'] + damageReductionFactor = stats.chargeModifiedAttributes['aoeDamageReductionFactor'] + explosionRadius = stats.chargeModifiedAttributes['aoeCloudSize'] + explosionVelocity = stats.chargeModifiedAttributes['aoeVelocity'] + typeing = 'Missile' + name = stats.item.name + ', ' + stats.charge.name + elif stats.hardpoint == 0: + aoeFieldRange = stats.itemModifiedAttributes['empFieldRange'] + typeing = 'SmartBomb' + name = stats.item.name + statDict = {'dps': stats.dps * c, 'capUse': stats.capUse * c, '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 * c, 'tracking': tracking,\ + 'maxVelocity': maxVelocity, 'explosionDelay': explosionDelay, 'damageReductionFactor': damageReductionFactor,\ + 'explosionRadius': explosionRadius, 'explosionVelocity': explosionVelocity, 'aoeFieldRange': aoeFieldRange\ + } + weaponSystems.append(statDict) + #if fit.droneDPS > 0: + for drone in fit.drones: + if drone.dps[0] > 0 and drone.amountActive > 0: + newTracking = drone.itemModifiedAttributes['trackingSpeed'] / (drone.itemModifiedAttributes['optimalSigRadius'] / 40000) + statDict = {'dps': drone.dps[0], 'cycleTime': drone.cycleTime, 'type': 'Drone',\ + 'optimal': drone.maxRange, 'name': drone.item.name, 'falloff': drone.falloff,\ + 'maxSpeed': drone.itemModifiedAttributes['maxVelocity'], 'tracking': newTracking,\ + 'volley': drone.dps[1]\ + } + weaponSystems.append(statDict) + for fighter in fit.fighters: + if fighter.dps[0] > 0 and fighter.amountActive > 0: + abilities = [] + #for ability in fighter.abilities: + if 'fighterAbilityAttackMissileDamageEM' in fighter.itemModifiedAttributes: + baseRef = 'fighterAbilityAttackMissile' + baseRefDam = baseRef + 'Damage' + abBaseDamage = fighter.itemModifiedAttributes[baseRefDam + 'EM'] + fighter.itemModifiedAttributes[baseRefDam + 'Therm'] + fighter.itemModifiedAttributes[baseRefDam + 'Exp'] + fighter.itemModifiedAttributes[baseRefDam + 'Kin'] + abDamage = abBaseDamage * fighter.itemModifiedAttributes[baseRefDam + 'Multiplier'] + ability = {'name': 'RegularAttack', 'volley': abDamage * fighter.amountActive, 'explosionRadius': fighter.itemModifiedAttributes[baseRef + 'ExplosionRadius'],\ + 'explosionVelocity': fighter.itemModifiedAttributes[baseRef + 'ExplosionVelocity'], 'optimal': fighter.itemModifiedAttributes[baseRef + 'RangeOptimal'],\ + 'damageReductionFactor': fighter.itemModifiedAttributes[baseRef + 'ReductionFactor'], 'rof': fighter.itemModifiedAttributes[baseRef + 'Duration'],\ + } + abilities.append(ability) + if 'fighterAbilityMissilesDamageEM' in fighter.itemModifiedAttributes: + baseRef = 'fighterAbilityMissiles' + baseRefDam = baseRef + 'Damage' + abBaseDamage = fighter.itemModifiedAttributes[baseRefDam + 'EM'] + fighter.itemModifiedAttributes[baseRefDam + 'Therm'] + fighter.itemModifiedAttributes[baseRefDam + 'Exp'] + fighter.itemModifiedAttributes[baseRefDam + 'Kin'] + abDamage = abBaseDamage * fighter.itemModifiedAttributes[baseRefDam + 'Multiplier'] + ability = {'name': 'MissileAttack', 'volley': abDamage * fighter.amountActive, 'explosionRadius': fighter.itemModifiedAttributes[baseRef + 'ExplosionRadius'],\ + 'explosionVelocity': fighter.itemModifiedAttributes[baseRef + 'ExplosionVelocity'], 'optimal': fighter.itemModifiedAttributes[baseRef + 'Range'],\ + 'damageReductionFactor': fighter.itemModifiedAttributes[baseRefDam + 'ReductionFactor'], 'rof': fighter.itemModifiedAttributes[baseRef + 'Duration'],\ + } + abilities.append(ability) + statDict = {'dps': fighter.dps[0], 'type': 'Fighter', 'name': fighter.item.name,\ + 'maxSpeed': fighter.itemModifiedAttributes['maxVelocity'], 'abilities': abilities, 'ehp': fighter.itemModifiedAttributes['shieldCapacity'] / 0.8875 * fighter.amountActive,\ + 'volley': fighter.dps[1], 'signatureRadius': fighter.itemModifiedAttributes['signatureRadius']\ + } + weaponSystems.append(statDict) + turretSlots = fit.ship.itemModifiedAttributes['turretSlotsLeft'] + launcherSlots = fit.ship.itemModifiedAttributes['launcherSlotsLeft'] + droneBandwidth = fit.ship.itemModifiedAttributes['droneBandwidth'] + if turretSlots == None: + turretSlots = 0 + if launcherSlots == None: + launcherSlots = 0 + if droneBandwidth == None: + droneBandwidth = 0 + effectiveTurretSlots = turretSlots + effectiveLauncherSlots = launcherSlots + effectiveDroneBandwidth = droneBandwidth + from eos.db import gamedata_session + from eos.gamedata import Traits + filter = Traits.typeID == fit.shipID + data = gamedata_session.query(Traits).options().filter(filter).all() + roleBonusMode = False + if len(data) != 0: + print data[0].traitText + for bonusText in data[0].traitText.splitlines(): + bonusText = bonusText.lower() + #print 'bonus text line: ' + bonusText + if 'per skill level' in bonusText: + roleBonusMode = False + if 'role bonus' in bonusText: + roleBonusMode = True + multi = 1 + if 'damage' in bonusText and not 'control' in bonusText: + splitText = bonusText.split('%') + if float(splitText[0]) > 0 == False: + pyfalog.error('damage bonus split did not parse correctly!') + if roleBonusMode: + addedMulti = float(splitText[0]) + else: + addedMulti = float(splitText[0]) * 5 + multi = 1 + (addedMulti / 100) + elif 'rate of fire' in bonusText: + splitText = bonusText.split('%') + if splitText[0] > 0 == False: + pyfalog.error('rate of fire bonus split did not parse correctly!') + if roleBonusMode: + rofMulti = float(splitText[0]) + else: + rofMulti = float(splitText[0]) * 5 + multi = 1 / (1 - (rofMulti / 100)) + if multi > 1: + if 'drone' in bonusText.lower(): + effectiveDroneBandwidth *= multi + elif 'turret' in bonusText.lower(): + effectiveTurretSlots *= multi + elif 'missile' in bonusText.lower(): + effectiveLauncherSlots *= multi + effectiveTurretSlots = round(effectiveTurretSlots, 2); + effectiveLauncherSlots = round(effectiveLauncherSlots, 2); + effectiveDroneBandwidth = round(effectiveDroneBandwidth, 2); + hullResonance = {'exp': fit.ship.itemModifiedAttributes['explosiveDamageResonance'], 'kin': fit.ship.itemModifiedAttributes['kineticDamageResonance'], \ + 'therm': fit.ship.itemModifiedAttributes['thermalDamageResonance'], 'em': fit.ship.itemModifiedAttributes['emDamageResonance']} + armorResonance = {'exp': fit.ship.itemModifiedAttributes['armorExplosiveDamageResonance'], 'kin': fit.ship.itemModifiedAttributes['armorKineticDamageResonance'], \ + 'therm': fit.ship.itemModifiedAttributes['armorThermalDamageResonance'], 'em': fit.ship.itemModifiedAttributes['armorEmDamageResonance']} + shieldResonance = {'exp': fit.ship.itemModifiedAttributes['shieldExplosiveDamageResonance'], 'kin': fit.ship.itemModifiedAttributes['shieldKineticDamageResonance'], \ + 'therm': fit.ship.itemModifiedAttributes['shieldThermalDamageResonance'], 'em': fit.ship.itemModifiedAttributes['shieldEmDamageResonance']} + resonance = {'hull': hullResonance, 'armor': armorResonance, 'shield': shieldResonance} + shipSizes = ['Frigate', 'Destroyer', 'Cruiser', 'Battlecruiser', 'Battleship', 'Capital', 'Industrial', 'Misc'] + if groupID in [25, 31, 237, 324, 830, 831, 834, 893, 1283, 1527]: + shipSize = shipSizes[0] + elif groupID in [420, 541, 1305, 1534]: + shipSize = shipSizes[1] + elif groupID in [26, 358, 832, 833, 894, 906, 963]: + shipSize = shipSizes[2] + elif groupID in [419, 540, 1201]: + shipSize = shipSizes[3] + elif groupID in [27, 381, 898, 900]: + shipSize = shipSizes[4] + elif groupID in [30, 485, 513, 547, 659, 883, 902, 1538]: + shipSize = shipSizes[5] + elif groupID in [28, 380, 1202, 463, 543, 941]: + shipSize = shipSizes[6] + elif groupID in [29, 1022]: + shipSize = shipSizes[7] + else: + shipSize = 'ShipSize not found for ' + fit.name + ' groupID: ' + str(groupID) + print shipSize + try: + parsable = {'name': fit.name, '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': fit.ship.itemModifiedAttributes['signatureRadius'],\ + 'weapons': weaponSystems, 'scanRes': fit.ship.itemModifiedAttributes['scanResolution'],\ + 'projectedModules': fit.projectedModules, 'capUsed': fit.capUsed, 'capRecharge': fit.capRecharge,\ + 'rigSlots': fit.ship.itemModifiedAttributes['rigSlots'], 'lowSlots': fit.ship.itemModifiedAttributes['lowSlots'],\ + 'midSlots': fit.ship.itemModifiedAttributes['medSlots'], 'highSlots': fit.ship.itemModifiedAttributes['hiSlots'],\ + 'turretSlots': fit.ship.itemModifiedAttributes['turretSlotsLeft'], 'launcherSlots': fit.ship.itemModifiedAttributes['launcherSlotsLeft'],\ + 'powerOutput': fit.ship.itemModifiedAttributes['powerOutput'], 'rigSize': fit.ship.itemModifiedAttributes['rigSize'],\ + 'effectiveTurrets': effectiveTurretSlots, 'effectiveLaunchers': effectiveLauncherSlots, 'effectiveDroneBandwidth': effectiveDroneBandwidth,\ + 'resonance': resonance, 'typeID': fit.shipID, 'groupID': groupID, 'shipSize': shipSize\ + } + except TypeError: + print 'Error parsing fit:' + str(fit) + print TypeError + parsable = {'name': fit.name + 'Fit could not be correctly parsed'} + #print fit.ship.itemModifiedAttributes.items() + #help(fit) + #if len(fit.fighters) > 5: + #print fit.fighters + #help(fit.fighters[0]) + stringified = json.dumps(parsable, skipkeys=True) + return stringified +def setFitFromString(dnaString, fitName, groupID) : + modArray = dnaString.split(':') + fitL = Fit() + print modArray[0] + fitID = fitL.newFit(int(modArray[0]), fitName) + fit = eos.db.getFit(fitID) + ammoArray = [] + n = -1 + for mod in iter(modArray): + n = n + 1 + if n > 0: + #print n + #print mod + modSp = mod.split(';') + if len(modSp) == 2: + k = 0 + while k < int(modSp[1]): + k = k + 1 + itemID = int(modSp[0]) + item = eos.db.getItem(int(modSp[0]), eager=("attributes", "group.category")) + cat = item.category.name + if cat == 'Drone': + fitL.addDrone(fitID, itemID, int(modSp[1]), recalc=False) + k += int(modSp[1]) + if cat == 'Fighter': + fitL.addFighter(fitID, itemID, recalc=False) + #fit.fighters.last.abilities.active = True + k += 100 + if fitL.isAmmo(int(modSp[0])): + k += 100 + ammoArray.append(int(modSp[0])); + fitL.appendModule(fitID, int(modSp[0])) + fit = eos.db.getFit(fitID) + #nonEmptyModules = fit.modules + #while nonEmptyModules.find(None) >= 0: + # print 'ssssssssssssssss' + # nonEmptyModules.remove(None) + for ammo in iter(ammoArray): + fitL.setAmmo(fitID, ammo, fit.modules) + if len(fit.drones) > 0: + fit.drones[0].amountActive = fit.drones[0].amount + eos.db.commit() + for fighter in iter(fit.fighters): + for ability in fighter.abilities: + if ability.effect.handlerName == u'fighterabilityattackm' and ability.active == True: + for abilityAltRef in fighter.abilities: + if abilityAltRef.effect.isImplemented: + abilityAltRef.active = True + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + #print fit.modules + #fit.calculateWeaponStats() + fitL.addCommandFit(fit.ID, armorLinkShip) + fitL.addCommandFit(fit.ID, shieldLinkShip) + fitL.addCommandFit(fit.ID, skirmishLinkShip) + fitL.addCommandFit(fit.ID, infoLinkShip) + #def anonfunc(unusedArg): True + jsonStr = parseNeededFitDetails(fit, groupID) + #print vars(fit.ship._Ship__item) + #help(fit) + Fit.deleteFit(fitID) + return jsonStr +launchUI = False +#launchUI = True +if launchUI == False: + from service.fit import Fit + #setFitFromString(dnaChim, 'moMachsD') + #help(eos.db.getItem) + #ship = es_Ship(eos.db.getItem(27)) + processExportedHtml('../.pyfa/pyfaFits.html') From 4b2a58ca6ff8765f855c2d1ac1be1f3f215536bf Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Sun, 11 Mar 2018 03:51:41 -0400 Subject: [PATCH 02/49] Updated .gitignore to include the generated export files --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index dcee5d692..ec0ef928d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +#Fit and ship export data generated by effs_stat_export.py +jsonShipBaseStatExport.js +jsonShipStatExport.js + #Python specific *.pyc From ed3083aa77aa00c03f6df964253741a3050364ce Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Mon, 19 Mar 2018 22:56:42 -0400 Subject: [PATCH 03/49] Fixed indentation issues and corrected fighters damage reduction factor to include sensitivity --- effs_stat_export.py | 67 ++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/effs_stat_export.py b/effs_stat_export.py index 604cc250e..3ce41e9b6 100755 --- a/effs_stat_export.py +++ b/effs_stat_export.py @@ -11,6 +11,8 @@ from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, import config +from math import log + try: import wxversion except ImportError: @@ -169,9 +171,9 @@ def processExportedHtml(fileLocation): outputBaseline.write(stats) outputBaseline.write(',\n') baseN += 1; - limit = 500 - skipTill = 0 - n = 0 + limit = 500 + skipTill = 0 + n = 0 try: with open('pyfaFits.html'): fileLocation = 'pyfaFits.html' @@ -235,7 +237,7 @@ def parseNeededFitDetails(fit, groupID): tracking = stats.itemModifiedAttributes['trackingSpeed'] typeing = 'Turret' name = stats.item.name + ', ' + stats.charge.name - elif stats.hardpoint == 1: + elif stats.hardpoint == 1 or 'Bomb Launcher' in stats.item.name: maxVelocity = stats.chargeModifiedAttributes['maxVelocity'] explosionDelay = stats.chargeModifiedAttributes['explosionDelay'] damageReductionFactor = stats.chargeModifiedAttributes['aoeDamageReductionFactor'] @@ -247,15 +249,15 @@ def parseNeededFitDetails(fit, groupID): aoeFieldRange = stats.itemModifiedAttributes['empFieldRange'] typeing = 'SmartBomb' name = stats.item.name - statDict = {'dps': stats.dps * c, 'capUse': stats.capUse * c, '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 * c, 'tracking': tracking,\ - 'maxVelocity': maxVelocity, 'explosionDelay': explosionDelay, 'damageReductionFactor': damageReductionFactor,\ - 'explosionRadius': explosionRadius, 'explosionVelocity': explosionVelocity, 'aoeFieldRange': aoeFieldRange\ - } - weaponSystems.append(statDict) - #if fit.droneDPS > 0: + statDict = {'dps': stats.dps * c, 'capUse': stats.capUse * c, '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 * c, 'tracking': tracking,\ + 'maxVelocity': maxVelocity, 'explosionDelay': explosionDelay, 'damageReductionFactor': damageReductionFactor,\ + 'explosionRadius': explosionRadius, 'explosionVelocity': explosionVelocity, 'aoeFieldRange': aoeFieldRange\ + } + weaponSystems.append(statDict) + #if fit.droneDPS > 0: for drone in fit.drones: if drone.dps[0] > 0 and drone.amountActive > 0: newTracking = drone.itemModifiedAttributes['trackingSpeed'] / (drone.itemModifiedAttributes['optimalSigRadius'] / 40000) @@ -266,34 +268,37 @@ def parseNeededFitDetails(fit, groupID): } weaponSystems.append(statDict) for fighter in fit.fighters: + print vars(fighter) if fighter.dps[0] > 0 and fighter.amountActive > 0: abilities = [] #for ability in fighter.abilities: if 'fighterAbilityAttackMissileDamageEM' in fighter.itemModifiedAttributes: baseRef = 'fighterAbilityAttackMissile' baseRefDam = baseRef + 'Damage' + damageReductionFactor = log(fighter.itemModifiedAttributes[baseRef + 'ReductionFactor']) / log(fighter.itemModifiedAttributes[baseRef + 'ReductionSensitivity']) abBaseDamage = fighter.itemModifiedAttributes[baseRefDam + 'EM'] + fighter.itemModifiedAttributes[baseRefDam + 'Therm'] + fighter.itemModifiedAttributes[baseRefDam + 'Exp'] + fighter.itemModifiedAttributes[baseRefDam + 'Kin'] abDamage = abBaseDamage * fighter.itemModifiedAttributes[baseRefDam + 'Multiplier'] ability = {'name': 'RegularAttack', 'volley': abDamage * fighter.amountActive, 'explosionRadius': fighter.itemModifiedAttributes[baseRef + 'ExplosionRadius'],\ 'explosionVelocity': fighter.itemModifiedAttributes[baseRef + 'ExplosionVelocity'], 'optimal': fighter.itemModifiedAttributes[baseRef + 'RangeOptimal'],\ - 'damageReductionFactor': fighter.itemModifiedAttributes[baseRef + 'ReductionFactor'], 'rof': fighter.itemModifiedAttributes[baseRef + 'Duration'],\ + 'damageReductionFactor': damageReductionFactor, 'rof': fighter.itemModifiedAttributes[baseRef + 'Duration'],\ } abilities.append(ability) if 'fighterAbilityMissilesDamageEM' in fighter.itemModifiedAttributes: baseRef = 'fighterAbilityMissiles' baseRefDam = baseRef + 'Damage' + damageReductionFactor = log(fighter.itemModifiedAttributes[baseRefDam + 'ReductionFactor']) / log(fighter.itemModifiedAttributes[baseRefDam + 'ReductionSensitivity']) abBaseDamage = fighter.itemModifiedAttributes[baseRefDam + 'EM'] + fighter.itemModifiedAttributes[baseRefDam + 'Therm'] + fighter.itemModifiedAttributes[baseRefDam + 'Exp'] + fighter.itemModifiedAttributes[baseRefDam + 'Kin'] abDamage = abBaseDamage * fighter.itemModifiedAttributes[baseRefDam + 'Multiplier'] ability = {'name': 'MissileAttack', 'volley': abDamage * fighter.amountActive, 'explosionRadius': fighter.itemModifiedAttributes[baseRef + 'ExplosionRadius'],\ 'explosionVelocity': fighter.itemModifiedAttributes[baseRef + 'ExplosionVelocity'], 'optimal': fighter.itemModifiedAttributes[baseRef + 'Range'],\ - 'damageReductionFactor': fighter.itemModifiedAttributes[baseRefDam + 'ReductionFactor'], 'rof': fighter.itemModifiedAttributes[baseRef + 'Duration'],\ + 'damageReductionFactor': damageReductionFactor, 'rof': fighter.itemModifiedAttributes[baseRef + 'Duration'],\ } abilities.append(ability) - statDict = {'dps': fighter.dps[0], 'type': 'Fighter', 'name': fighter.item.name,\ - 'maxSpeed': fighter.itemModifiedAttributes['maxVelocity'], 'abilities': abilities, 'ehp': fighter.itemModifiedAttributes['shieldCapacity'] / 0.8875 * fighter.amountActive,\ - 'volley': fighter.dps[1], 'signatureRadius': fighter.itemModifiedAttributes['signatureRadius']\ - } - weaponSystems.append(statDict) + statDict = {'dps': fighter.dps[0], 'type': 'Fighter', 'name': fighter.item.name,\ + 'maxSpeed': fighter.itemModifiedAttributes['maxVelocity'], 'abilities': abilities, 'ehp': fighter.itemModifiedAttributes['shieldCapacity'] / 0.8875 * fighter.amountActive,\ + 'volley': fighter.dps[1], 'signatureRadius': fighter.itemModifiedAttributes['signatureRadius']\ + } + weaponSystems.append(statDict) turretSlots = fit.ship.itemModifiedAttributes['turretSlotsLeft'] launcherSlots = fit.ship.itemModifiedAttributes['launcherSlotsLeft'] droneBandwidth = fit.ship.itemModifiedAttributes['droneBandwidth'] @@ -433,14 +438,14 @@ def setFitFromString(dnaString, fitName, groupID) : if fitL.isAmmo(int(modSp[0])): k += 100 ammoArray.append(int(modSp[0])); - fitL.appendModule(fitID, int(modSp[0])) - fit = eos.db.getFit(fitID) - #nonEmptyModules = fit.modules - #while nonEmptyModules.find(None) >= 0: - # print 'ssssssssssssssss' - # nonEmptyModules.remove(None) + fitL.appendModule(fitID, int(modSp[0])) + fit = eos.db.getFit(fitID) + #nonEmptyModules = fit.modules + #while nonEmptyModules.find(None) >= 0: + # print 'ssssssssssssssss' + # nonEmptyModules.remove(None) for ammo in iter(ammoArray): - fitL.setAmmo(fitID, ammo, fit.modules) + fitL.setAmmo(fitID, ammo, filter(lambda mod: str(mod).find('name') > 0, fit.modules)) if len(fit.drones) > 0: fit.drones[0].amountActive = fit.drones[0].amount eos.db.commit() @@ -450,10 +455,10 @@ def setFitFromString(dnaString, fitName, groupID) : for abilityAltRef in fighter.abilities: if abilityAltRef.effect.isImplemented: abilityAltRef.active = True - fitL.recalc(fit) - fit = eos.db.getFit(fitID) - #print fit.modules - #fit.calculateWeaponStats() + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + #print fit.modules + #fit.calculateWeaponStats() fitL.addCommandFit(fit.ID, armorLinkShip) fitL.addCommandFit(fit.ID, shieldLinkShip) fitL.addCommandFit(fit.ID, skirmishLinkShip) From aec9202be1eedaf540b92af7bbb7dca9d9c6c050 Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Thu, 5 Apr 2018 02:24:44 -0400 Subject: [PATCH 04/49] added more data to effs exports, including module names. --- effs_stat_export.py | 292 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 265 insertions(+), 27 deletions(-) diff --git a/effs_stat_export.py b/effs_stat_export.py index 3ce41e9b6..411fdbb86 100755 --- a/effs_stat_export.py +++ b/effs_stat_export.py @@ -97,9 +97,9 @@ skirmishLinkShip = eos.db.searchFits('skirmish links')[0] import json def processExportedHtml(fileLocation): - output = open('./jsonShipStatExport.js', 'w') + output = open('./shipJSON.js', 'w') output.write('let shipJSON = JSON.stringify([') - outputBaseline = open('./jsonShipBaseStatExport.js', 'w') + outputBaseline = open('./shipBaseJSON.js', 'w') outputBaseline.write('let shipBaseJSON = JSON.stringify([') shipCata = eos.db.getItemsByCategory('Ship') #shipCata = eos.db.getItem(638) @@ -158,21 +158,19 @@ def processExportedHtml(fileLocation): print 'Not a list of dicts' #print vars(shipCata._sa_instance_state) - baseLimit = 500 + baseLimit = 0 baseN = 0 + nameReqBase = ''; for ship in iter(shipCata): - if baseN < baseLimit: - #print ship - #print ship.ID - #print ship.categoryName - #print vars(ship) + if baseN < baseLimit and nameReqBase in ship.name: dna = str(ship.ID) stats = setFitFromString(dna, ship.name, ship.groupID) outputBaseline.write(stats) outputBaseline.write(',\n') baseN += 1; - limit = 500 + limit = 0 skipTill = 0 + nameReq = '' n = 0 try: with open('pyfaFits.html'): @@ -191,7 +189,17 @@ def processExportedHtml(fileLocation): fileLocation = '../../.pyfa/pyfaFits.html' except: fileLocation = None; - if fileLocation != None: + fitList = eos.db.getFitList() + with open(fileLocation) as f: + for fit in fitList: + if limit == None or n < limit: + n += 1 + name = fit.ship.name + ': ' + fit.name + if n >= skipTill and nameReq in name: + stats = parseNeededFitDetails(fit, 0) + output.write(stats) + output.write(',\n') + if False and fileLocation != None: with open(fileLocation) as f: for fullLine in f: if limit == None or n < limit: @@ -201,7 +209,7 @@ def processExportedHtml(fileLocation): endInd = line.find('::') dna = line[0:endInd] name = line[line.find('>') + 1:line.find('<')] - if n >= skipTill: + if n >= skipTill and nameReq in name: print 'name: ' + name + ' DNA: ' + dna stats = setFitFromString(dna, name, 0) output.write(stats) @@ -210,19 +218,227 @@ def processExportedHtml(fileLocation): output.close() outputBaseline.write(']);\nexport {shipBaseJSON};') outputBaseline.close() +def attrDirectMap(values, target, source): + for val in values: + target[val] = source.itemModifiedAttributes[val] def parseNeededFitDetails(fit, groupID): singleRunPrintPreformed = False weaponSystems = [] groups = {} - #help(fit.modules) - #help(fit.modules[0]) + moduleNames = [] + fitID = fit.ID + if len(fit.modules) > 0: + fit.name = fit.ship.name + ': ' + fit.name + print '' + print 'name: ' + fit.name + fitL = Fit() + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + if False: + from eos.db import gamedata_session + from eos.gamedata import Group, Category + filterVal = Group.categoryID == 6 + data = gamedata_session.query(Group).options().filter(filterVal).all() + for group in data: + print group.groupName + ' groupID: ' + str(group.groupID) + #print group.categoryName + ' categoryID: ' + str(group.categoryID) + ', published: ' + str(group.published) + #print vars(group) + #print '' + return '' + projectedModGroupIds = [ + 41, 52, 65, 67, 68, 71, 80, 201, 208, 291, 325, 379, 585, + 842, 899, 1150, 1154, 1189, 1306, 1672, 1697, 1698, 1815, 1894 + ] + projectedMods = filter(lambda mod: mod.item and mod.item.groupID in projectedModGroupIds, fit.modules) + + unpropedSpeed = fit.maxSpeed + unpropedSig = fit.ship.itemModifiedAttributes['signatureRadius'] + usingMWD = False + propMods = filter(lambda mod: mod.item and mod.item.groupID in [46], fit.modules) + possibleMWD = filter(lambda mod: 'signatureRadiusBonus' in mod.item.attributes, propMods) + if len(possibleMWD) > 0 and possibleMWD[0].state > 0: + mwd = possibleMWD[0] + oldMwdState = mwd.state + mwd.state = 0 + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + unpropedSpeed = fit.maxSpeed + unpropedSig = fit.ship.itemModifiedAttributes['signatureRadius'] + mwd.state = oldMwdState + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + usingMWD = True + + print fit.ship.itemModifiedAttributes['rigSize'] + print propMods + mwdPropSpeed = fit.maxSpeed + if groupID > 0: + propID = None + rigSize = fit.ship.itemModifiedAttributes['rigSize'] + if rigSize == 1 and fit.ship.itemModifiedAttributes['medSlots'] > 0: + propID = 440 + elif rigSize == 2 and fit.ship.itemModifiedAttributes['medSlots'] > 0: + propID = 12076 + elif rigSize == 3 and fit.ship.itemModifiedAttributes['medSlots'] > 0: + propID = 12084 + elif rigSize == 4 and fit.ship.itemModifiedAttributes['medSlots'] > 0: + if fit.ship.itemModifiedAttributes['powerOutput'] > 60000: + propID = 41253 + else: + propID = 12084 + elif rigSize == None and fit.ship.itemModifiedAttributes['medSlots'] > 0: + propID = 440 + if propID: + fitL.appendModule(fitID, propID) + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + mwdPropSpeed = fit.maxSpeed + mwdPosition = filter(lambda mod: mod.item and mod.item.ID == propID, fit.modules)[0].position + fitL.removeModule(fitID, mwdPosition) + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + + projections = [] + for mod in projectedMods: + stats = {} + if mod.item.groupID == 65 or mod.item.groupID == 1672: + stats['type'] = 'Stasis Web' + stats['optimal'] = mod.itemModifiedAttributes['maxRange'] + attrDirectMap(['duration', 'speedFactor'], stats, mod) + elif mod.item.groupID == 291: + stats['type'] = 'Weapon Disruptor' + stats['optimal'] = mod.itemModifiedAttributes['maxRange'] + stats['falloff'] = mod.itemModifiedAttributes['falloffEffectiveness'] + attrDirectMap([ + 'trackingSpeedBonus', 'maxRangeBonus', 'falloffBonus', 'aoeCloudSizeBonus',\ + 'aoeVelocityBonus', 'missileVelocityBonus', 'explosionDelayBonus'\ + ], stats, mod) + elif mod.item.groupID == 68: + stats['type'] = 'Energy Nosferatu' + attrDirectMap(['powerTransferAmount', 'energyNeutralizerSignatureResolution'], stats, mod) + elif mod.item.groupID == 71: + stats['type'] = 'Energy Neutralizer' + attrDirectMap([ + 'energyNeutralizerSignatureResolution','entityCapacitorLevelModifierSmall',\ + 'entityCapacitorLevelModifierMedium', 'entityCapacitorLevelModifierLarge',\ + 'energyNeutralizerAmount'\ + ], stats, mod) + elif mod.item.groupID == 41 or mod.item.groupID == 1697: + stats['type'] = 'Remote Shield Booster' + attrDirectMap(['shieldBonus'], stats, mod) + elif mod.item.groupID == 325 or mod.item.groupID == 1698: + stats['type'] = 'Remote Armor Repairer' + attrDirectMap(['armorDamageAmount'], stats, mod) + elif mod.item.groupID == 52: + stats['type'] = 'Warp Scrambler' + attrDirectMap(['activationBlockedStrenght', 'warpScrambleStrength'], stats, mod) + elif mod.item.groupID == 379: + stats['type'] = 'Target Painter' + attrDirectMap(['signatureRadiusBonus'], stats, mod) + elif mod.item.groupID == 208: + stats['type'] = 'Sensor Dampener' + attrDirectMap(['maxTargetRangeBonus', 'scanResolutionBonus'], stats, mod) + elif mod.item.groupID == 201: + stats['type'] = 'ECM' + attrDirectMap([ + 'scanGravimetricStrengthBonus', 'scanMagnetometricStrengthBonus',\ + 'scanRadarStrengthBonus', 'scanLadarStrengthBonus',\ + ], stats, mod) + elif mod.item.groupID == 80: + stats['type'] = 'Burst Jammer' + mod.itemModifiedAttributes['maxRange'] = mod.itemModifiedAttributes['ecmBurstRange'] + attrDirectMap([ + 'scanGravimetricStrengthBonus', 'scanMagnetometricStrengthBonus',\ + 'scanRadarStrengthBonus', 'scanLadarStrengthBonus',\ + ], stats, mod) + elif mod.item.groupID == 1189: + stats['type'] = 'Micro Jump Drive' + mod.itemModifiedAttributes['maxRange'] = 0 + attrDirectMap(['moduleReactivationDelay'], stats, mod) + if mod.itemModifiedAttributes['maxRange'] == None: + print mod.item.name + print mod.itemModifiedAttributes.items() + raise ValueError('Projected module lacks a maxRange') + stats['optimal'] = mod.itemModifiedAttributes['maxRange'] + stats['falloff'] = mod.itemModifiedAttributes['falloffEffectiveness'] or 0 + attrDirectMap(['duration', 'capacitorNeed'], stats, mod) + projections.append(stats) + #print '' + #print stats + #print mod.item.name + #print mod.itemModifiedAttributes.items() + #print '' + #print vars(mod.item) + #print vars(web.itemModifiedAttributes) + #print vars(fit.modules) + #print vars(fit.modules[0]) + highSlotNames = [] + midSlotNames = [] + lowSlotNames = [] + rigSlotNames = [] + miscSlotNames = [] #subsystems ect for mod in fit.modules: + if mod.slot == 3: + modSlotNames = highSlotNames + elif mod.slot == 2: + modSlotNames = midSlotNames + elif mod.slot == 1: + modSlotNames = lowSlotNames + elif mod.slot == 4: + modSlotNames = rigSlotNames + elif mod.slot == 5: + modSlotNames = miscSlotNames + try: + if mod.item != None: + if mod.charge != None: + modSlotNames.append(mod.item.name + ': ' + mod.charge.name) + else: + modSlotNames.append(mod.item.name) + else: + modSlotNames.append('Empty Slot') + except: + print vars(mod) + print 'could not find name for module' + print fit.modules if mod.dps > 0: keystr = str(mod.itemID) + '-' + str(mod.chargeID) if keystr in groups: groups[keystr][1] += 1 else: groups[keystr] = [mod, 1] + for modInfo in [['High Slots:'], highSlotNames, ['', 'Med Slots:'], midSlotNames, ['', 'Low Slots:'], lowSlotNames, ['', 'Rig Slots:'], rigSlotNames]: + moduleNames.extend(modInfo) + if len(miscSlotNames) > 0: + moduleNames.append('') + moduleNames.append('Subsystems:') + moduleNames.extend(miscSlotNames) + droneNames = [] + fighterNames = [] + for drone in fit.drones: + if drone.amountActive > 0: + droneNames.append(drone.item.name) + for fighter in fit.fighters: + if fighter.amountActive > 0: + fighterNames.append(fighter.item.name) + if len(droneNames) > 0: + moduleNames.append('') + moduleNames.append('Drones:') + moduleNames.extend(droneNames) + if len(fighterNames) > 0: + moduleNames.append('') + moduleNames.append('Fighters:') + moduleNames.extend(fighterNames) + if len(fit.implants) > 0: + moduleNames.append('') + moduleNames.append('Implants:') + for implant in fit.implants: + moduleNames.append(implant.item.name) + if len(fit.commandFits) > 0: + moduleNames.append('') + moduleNames.append('Command Fits:') + for commandFit in fit.commandFits: + moduleNames.append(commandFit.name) + for wepGroup in groups: stats = groups[wepGroup][0] c = groups[wepGroup][1] @@ -268,7 +484,6 @@ def parseNeededFitDetails(fit, groupID): } weaponSystems.append(statDict) for fighter in fit.fighters: - print vars(fighter) if fighter.dps[0] > 0 and fighter.amountActive > 0: abilities = [] #for ability in fighter.abilities: @@ -313,32 +528,46 @@ def parseNeededFitDetails(fit, groupID): effectiveDroneBandwidth = droneBandwidth from eos.db import gamedata_session from eos.gamedata import Traits - filter = Traits.typeID == fit.shipID - data = gamedata_session.query(Traits).options().filter(filter).all() + filterVal = Traits.typeID == fit.shipID + data = gamedata_session.query(Traits).options().filter(filterVal).all() roleBonusMode = False if len(data) != 0: - print data[0].traitText + #print data[0].traitText + previousTypedBonus = 0 + previousDroneTypeBonus = 0 for bonusText in data[0].traitText.splitlines(): bonusText = bonusText.lower() #print 'bonus text line: ' + bonusText if 'per skill level' in bonusText: roleBonusMode = False - if 'role bonus' in bonusText: + if 'role bonus' in bonusText or 'misc bonus' in bonusText: roleBonusMode = True multi = 1 - if 'damage' in bonusText and not 'control' in bonusText: + if 'damage' in bonusText and not any(e in bonusText for e in ['control', 'heat']):#'control' in bonusText and not 'heat' in bonusText: splitText = bonusText.split('%') - if float(splitText[0]) > 0 == False: - pyfalog.error('damage bonus split did not parse correctly!') + if (float(splitText[0]) > 0) == False: + print 'damage bonus split did not parse correctly!' + print float(splitText[0]) if roleBonusMode: addedMulti = float(splitText[0]) else: addedMulti = float(splitText[0]) * 5 + if any(e in bonusText for e in [' em', 'thermal', 'kinetic', 'explosive']): + if addedMulti > previousTypedBonus: + previousTypedBonus = addedMulti + else: + addedMulti = 0 + if any(e in bonusText for e in ['heavy drone', 'medium drone', 'light drone', 'sentry drone']): + if addedMulti > previousDroneTypeBonus: + previousDroneTypeBonus = addedMulti + else: + addedMulti = 0 multi = 1 + (addedMulti / 100) elif 'rate of fire' in bonusText: splitText = bonusText.split('%') - if splitText[0] > 0 == False: - pyfalog.error('rate of fire bonus split did not parse correctly!') + if (float(splitText[0]) > 0) == False: + print 'rate of fire bonus split did not parse correctly!' + print float(splitText[0]) if roleBonusMode: rofMulti = float(splitText[0]) else: @@ -349,8 +578,11 @@ def parseNeededFitDetails(fit, groupID): effectiveDroneBandwidth *= multi elif 'turret' in bonusText.lower(): effectiveTurretSlots *= multi - elif 'missile' in bonusText.lower(): + elif any(e in bonusText for e in ['missile', 'torpedo']): effectiveLauncherSlots *= multi + if groupID == 485: + effectiveTurretSlots *= 9.4 + effectiveLauncherSlots *= 15 effectiveTurretSlots = round(effectiveTurretSlots, 2); effectiveLauncherSlots = round(effectiveLauncherSlots, 2); effectiveDroneBandwidth = round(effectiveDroneBandwidth, 2); @@ -394,7 +626,10 @@ def parseNeededFitDetails(fit, groupID): 'turretSlots': fit.ship.itemModifiedAttributes['turretSlotsLeft'], 'launcherSlots': fit.ship.itemModifiedAttributes['launcherSlotsLeft'],\ 'powerOutput': fit.ship.itemModifiedAttributes['powerOutput'], 'rigSize': fit.ship.itemModifiedAttributes['rigSize'],\ 'effectiveTurrets': effectiveTurretSlots, 'effectiveLaunchers': effectiveLauncherSlots, 'effectiveDroneBandwidth': effectiveDroneBandwidth,\ - 'resonance': resonance, 'typeID': fit.shipID, 'groupID': groupID, 'shipSize': shipSize\ + 'resonance': resonance, 'typeID': fit.shipID, 'groupID': groupID, 'shipSize': shipSize,\ + 'droneControlRange': fit.ship.itemModifiedAttributes['droneControlRange'], 'mass': fit.ship.itemModifiedAttributes['mass'],\ + 'moduleNames': moduleNames, 'projections': projections, 'unpropedSpeed': unpropedSpeed, 'unpropedSig': unpropedSig,\ + 'usingMWD': usingMWD, 'mwdPropSpeed': mwdPropSpeed } except TypeError: print 'Error parsing fit:' + str(fit) @@ -409,6 +644,9 @@ def parseNeededFitDetails(fit, groupID): return stringified def setFitFromString(dnaString, fitName, groupID) : modArray = dnaString.split(':') + additionalModeFit = '' + #if groupID == 485 and len(modArray) == 1: + #additionalModeFit = ',\n' + setFitFromString(dnaString + ':4292', fitName + ' (Sieged)', groupID) fitL = Fit() print modArray[0] fitID = fitL.newFit(int(modArray[0]), fitName) @@ -457,7 +695,7 @@ def setFitFromString(dnaString, fitName, groupID) : abilityAltRef.active = True fitL.recalc(fit) fit = eos.db.getFit(fitID) - #print fit.modules + print filter(lambda mod: mod.item.groupID in [1189, 658], fit.modules) #fit.calculateWeaponStats() fitL.addCommandFit(fit.ID, armorLinkShip) fitL.addCommandFit(fit.ID, shieldLinkShip) @@ -468,7 +706,7 @@ def setFitFromString(dnaString, fitName, groupID) : #print vars(fit.ship._Ship__item) #help(fit) Fit.deleteFit(fitID) - return jsonStr + return jsonStr + additionalModeFit launchUI = False #launchUI = True if launchUI == False: From 49b1e2ee36bad16dd81c951b19a1e9038168cf0d Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Thu, 5 Apr 2018 02:35:12 -0400 Subject: [PATCH 05/49] Added option to copy EFFS stats to the clipboard via the CopySelectDialog UI --- gui/copySelectDialog.py | 6 ++++-- gui/mainFrame.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 57ce48f47..95f6db57b 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -29,19 +29,21 @@ class CopySelectDialog(wx.Dialog): copyFormatDna = 3 copyFormatCrest = 4 copyFormatMultiBuy = 5 + copyFormatEffs = 6 def __init__(self, parent): wx.Dialog.__init__(self, parent, id=wx.ID_ANY, title=u"Select a format", size=(-1, -1), style=wx.DEFAULT_DIALOG_STYLE) mainSizer = wx.BoxSizer(wx.VERTICAL) - copyFormats = [u"EFT", u"EFT (Implants)", u"XML", u"DNA", u"CREST", u"MultiBuy"] + copyFormats = [u"EFT", u"EFT (Implants)", u"XML", u"DNA", u"CREST", u"MultiBuy", u"EFFS"] copyFormatTooltips = {CopySelectDialog.copyFormatEft: u"EFT text format", CopySelectDialog.copyFormatEftImps: u"EFT text format", CopySelectDialog.copyFormatXml: u"EVE native XML format", CopySelectDialog.copyFormatDna: u"A one-line text format", CopySelectDialog.copyFormatCrest: u"A JSON format used for EVE CREST", - CopySelectDialog.copyFormatMultiBuy: u"MultiBuy text format"} + CopySelectDialog.copyFormatMultiBuy: u"MultiBuy text format", + CopySelectDialog.copyFormatEffs: u"EFFS json stats format"} selector = wx.RadioBox(self, wx.ID_ANY, label=u"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 d4b23f3a4..fbd814670 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -77,6 +77,8 @@ from eos.db.saveddata.queries import getFit as db_getFit from service.port import Port, IPortUser from service.settings import HTMLExportSettings +from effs_stat_export import parseNeededFitDetails as exportEffsStats + from time import gmtime, strftime import threading @@ -754,6 +756,10 @@ class MainFrame(wx.Frame, IPortUser): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportMultiBuy(fit)) + def clipboardEffs(self): + fit = db_getFit(self.getActiveFit()) + toClipboard(exportEffsStats(fit, 0)) + def importFromClipboard(self, event): clipboard = fromClipboard() try: @@ -769,11 +775,11 @@ class MainFrame(wx.Frame, IPortUser): CopySelectDialog.copyFormatXml: self.clipboardXml, CopySelectDialog.copyFormatDna: self.clipboardDna, CopySelectDialog.copyFormatCrest: self.clipboardCrest, - CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy} + CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy, + CopySelectDialog.copyFormatEffs: self.clipboardEffs} dlg = CopySelectDialog(self) dlg.ShowModal() selected = dlg.GetSelected() - CopySelectDict[selected]() try: From acade567694f7477f2c41571ebdbedfc49d47c03 Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Mon, 30 Apr 2018 20:38:38 -0400 Subject: [PATCH 06/49] Adjusted effs fit name prefixing --- Rapier - 1 TP 1 Web Rapier w hml.xml | 25 +++++++++++++++++++++++++ effs_stat_export.py | 10 ++++++---- eos/events.py | 2 +- shipBaseJSON.js | 2 ++ shipJSON.js | 2 ++ 5 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 Rapier - 1 TP 1 Web Rapier w hml.xml create mode 100644 shipBaseJSON.js create mode 100644 shipJSON.js diff --git a/Rapier - 1 TP 1 Web Rapier w hml.xml b/Rapier - 1 TP 1 Web Rapier w hml.xml new file mode 100644 index 000000000..9ba601692 --- /dev/null +++ b/Rapier - 1 TP 1 Web Rapier w hml.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/effs_stat_export.py b/effs_stat_export.py index 411fdbb86..4acc6b233 100755 --- a/effs_stat_export.py +++ b/effs_stat_export.py @@ -228,7 +228,9 @@ def parseNeededFitDetails(fit, groupID): moduleNames = [] fitID = fit.ID if len(fit.modules) > 0: - fit.name = fit.ship.name + ': ' + fit.name + fitName = fit.ship.name + ': ' + fit.name + else: + fitName = fit.name print '' print 'name: ' + fit.name fitL = Fit() @@ -611,10 +613,10 @@ def parseNeededFitDetails(fit, groupID): elif groupID in [29, 1022]: shipSize = shipSizes[7] else: - shipSize = 'ShipSize not found for ' + fit.name + ' groupID: ' + str(groupID) + shipSize = 'ShipSize not found for ' + fitName + ' groupID: ' + str(groupID) print shipSize try: - parsable = {'name': fit.name, 'ehp': fit.ehp, 'droneDPS': fit.droneDPS, \ + parsable = {'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,\ @@ -634,7 +636,7 @@ def parseNeededFitDetails(fit, groupID): except TypeError: print 'Error parsing fit:' + str(fit) print TypeError - parsable = {'name': fit.name + 'Fit could not be correctly parsed'} + parsable = {'name': fitName + 'Fit could not be correctly parsed'} #print fit.ship.itemModifiedAttributes.items() #help(fit) #if len(fit.fighters) > 5: diff --git a/eos/events.py b/eos/events.py index de8bdfadb..eabdf0b7e 100644 --- a/eos/events.py +++ b/eos/events.py @@ -58,7 +58,7 @@ def rel_listener(target, value, initiator): if not target or (isinstance(value, Module) and value.isEmpty): return - print "{} has had a relationship change :D".format(target) + #print "{} has had a relationship change :D".format(target) target.modified = datetime.datetime.now() diff --git a/shipBaseJSON.js b/shipBaseJSON.js new file mode 100644 index 000000000..28b7228b2 --- /dev/null +++ b/shipBaseJSON.js @@ -0,0 +1,2 @@ +let shipBaseJSON = JSON.stringify([]); +export {shipBaseJSON}; \ No newline at end of file diff --git a/shipJSON.js b/shipJSON.js new file mode 100644 index 000000000..22c2ed09f --- /dev/null +++ b/shipJSON.js @@ -0,0 +1,2 @@ +let shipJSON = JSON.stringify([]); +export {shipJSON}; \ No newline at end of file From 090065ddd4657c59eb84a3609d8266a92790e9bf Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Mon, 30 Apr 2018 20:44:14 -0400 Subject: [PATCH 07/49] Removed sepurflous effs related files --- .gitignore | 4 ++-- Rapier - 1 TP 1 Web Rapier w hml.xml | 25 ------------------------- 2 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 Rapier - 1 TP 1 Web Rapier w hml.xml diff --git a/.gitignore b/.gitignore index ec0ef928d..f61f7692b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ #Fit and ship export data generated by effs_stat_export.py -jsonShipBaseStatExport.js -jsonShipStatExport.js +shipBaseJSON.js +shipJSON.js #Python specific *.pyc diff --git a/Rapier - 1 TP 1 Web Rapier w hml.xml b/Rapier - 1 TP 1 Web Rapier w hml.xml deleted file mode 100644 index 9ba601692..000000000 --- a/Rapier - 1 TP 1 Web Rapier w hml.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - From dba86edff24c526422af69208ad52af7bb56fad0 Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Wed, 2 May 2018 03:25:20 -0400 Subject: [PATCH 08/49] Added python3 functionality to effs exporter --- effs_stat_export.py | 108 ++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/effs_stat_export.py b/effs_stat_export.py index 4acc6b233..91248276a 100755 --- a/effs_stat_export.py +++ b/effs_stat_export.py @@ -90,10 +90,10 @@ if not os.path.exists(config.savePath): eos.db.saveddata_meta.create_all() -armorLinkShip = eos.db.searchFits('armor links')[0] -infoLinkShip = eos.db.searchFits('information links')[0] -shieldLinkShip = eos.db.searchFits('shield links')[0] -skirmishLinkShip = eos.db.searchFits('skirmish links')[0] +#armorLinkShip = eos.db.searchFits('armor links')[0] +#infoLinkShip = eos.db.searchFits('information links')[0] +#shieldLinkShip = eos.db.searchFits('shield links')[0] +#skirmishLinkShip = eos.db.searchFits('skirmish links')[0] import json def processExportedHtml(fileLocation): @@ -133,31 +133,31 @@ def processExportedHtml(fileLocation): things = []#[Category, MetaGroup, AttributeInfo, MetaData, Item, Attribute, Effect, ItemEffect, Traits]#, Attribute] if False: for dataTab in things : - print 'Data for: ' + str(dataTab) + print('Data for: ' + str(dataTab)) try: filter = dataTab.typeID == 638 except: filter = dataTab.ID == 638 data = gamedata_session.query(dataTab).options().filter(filter).all() - print data + print(data) try: varDict = vars(data) - print varDict + print(varDict) except: - print 'Not a Dict' + print('Not a Dict') try: varDict = data.__doc__ - print varDict + print(varDict) except: - print 'No items()' + print('No items()') try: for varDict in data: - print varDict - print vars(varDict) + print(varDict) + print(vars(varDict)) except: - print 'Not a list of dicts' + print('Not a list of dicts') - #print vars(shipCata._sa_instance_state) + #print(vars(shipCata._sa_instance_state)) baseLimit = 0 baseN = 0 nameReqBase = ''; @@ -210,7 +210,7 @@ def processExportedHtml(fileLocation): dna = line[0:endInd] name = line[line.find('>') + 1:line.find('<')] if n >= skipTill and nameReq in name: - print 'name: ' + name + ' DNA: ' + dna + print('name: ' + name + ' DNA: ' + dna) stats = setFitFromString(dna, name, 0) output.write(stats) output.write(',\n') @@ -231,8 +231,8 @@ def parseNeededFitDetails(fit, groupID): fitName = fit.ship.name + ': ' + fit.name else: fitName = fit.name - print '' - print 'name: ' + fit.name + print('') + print('name: ' + fit.name) fitL = Fit() fitL.recalc(fit) fit = eos.db.getFit(fitID) @@ -242,10 +242,10 @@ def parseNeededFitDetails(fit, groupID): filterVal = Group.categoryID == 6 data = gamedata_session.query(Group).options().filter(filterVal).all() for group in data: - print group.groupName + ' groupID: ' + str(group.groupID) - #print group.categoryName + ' categoryID: ' + str(group.categoryID) + ', published: ' + str(group.published) - #print vars(group) - #print '' + print(group.groupName + ' groupID: ' + str(group.groupID)) + #print(group.categoryName + ' categoryID: ' + str(group.categoryID) + ', published: ' + str(group.published) + #print(vars(group) + #print('' return '' projectedModGroupIds = [ 41, 52, 65, 67, 68, 71, 80, 201, 208, 291, 325, 379, 585, @@ -271,8 +271,8 @@ def parseNeededFitDetails(fit, groupID): fit = eos.db.getFit(fitID) usingMWD = True - print fit.ship.itemModifiedAttributes['rigSize'] - print propMods + print(fit.ship.itemModifiedAttributes['rigSize']) + print(propMods) mwdPropSpeed = fit.maxSpeed if groupID > 0: propID = None @@ -358,22 +358,22 @@ def parseNeededFitDetails(fit, groupID): mod.itemModifiedAttributes['maxRange'] = 0 attrDirectMap(['moduleReactivationDelay'], stats, mod) if mod.itemModifiedAttributes['maxRange'] == None: - print mod.item.name - print mod.itemModifiedAttributes.items() + print(mod.item.name) + print(mod.itemModifiedAttributes.items()) raise ValueError('Projected module lacks a maxRange') stats['optimal'] = mod.itemModifiedAttributes['maxRange'] stats['falloff'] = mod.itemModifiedAttributes['falloffEffectiveness'] or 0 attrDirectMap(['duration', 'capacitorNeed'], stats, mod) projections.append(stats) - #print '' - #print stats - #print mod.item.name - #print mod.itemModifiedAttributes.items() - #print '' - #print vars(mod.item) - #print vars(web.itemModifiedAttributes) - #print vars(fit.modules) - #print vars(fit.modules[0]) + #print('' + #print(stats + #print(mod.item.name + #print(mod.itemModifiedAttributes.items() + #print('' + #print(vars(mod.item) + #print(vars(web.itemModifiedAttributes) + #print(vars(fit.modules) + #print(vars(fit.modules[0]) highSlotNames = [] midSlotNames = [] lowSlotNames = [] @@ -399,9 +399,9 @@ def parseNeededFitDetails(fit, groupID): else: modSlotNames.append('Empty Slot') except: - print vars(mod) - print 'could not find name for module' - print fit.modules + print(vars(mod)) + print('could not find name for module') + print(fit.modules) if mod.dps > 0: keystr = str(mod.itemID) + '-' + str(mod.chargeID) if keystr in groups: @@ -534,12 +534,12 @@ def parseNeededFitDetails(fit, groupID): data = gamedata_session.query(Traits).options().filter(filterVal).all() roleBonusMode = False if len(data) != 0: - #print data[0].traitText + #print(data[0].traitText previousTypedBonus = 0 previousDroneTypeBonus = 0 for bonusText in data[0].traitText.splitlines(): bonusText = bonusText.lower() - #print 'bonus text line: ' + bonusText + #print('bonus text line: ' + bonusText if 'per skill level' in bonusText: roleBonusMode = False if 'role bonus' in bonusText or 'misc bonus' in bonusText: @@ -548,8 +548,8 @@ def parseNeededFitDetails(fit, groupID): if 'damage' in bonusText and not any(e in bonusText for e in ['control', 'heat']):#'control' in bonusText and not 'heat' in bonusText: splitText = bonusText.split('%') if (float(splitText[0]) > 0) == False: - print 'damage bonus split did not parse correctly!' - print float(splitText[0]) + print('damage bonus split did not parse correctly!') + print(float(splitText[0])) if roleBonusMode: addedMulti = float(splitText[0]) else: @@ -568,8 +568,8 @@ def parseNeededFitDetails(fit, groupID): elif 'rate of fire' in bonusText: splitText = bonusText.split('%') if (float(splitText[0]) > 0) == False: - print 'rate of fire bonus split did not parse correctly!' - print float(splitText[0]) + print('rate of fire bonus split did not parse correctly!') + print(float(splitText[0])) if roleBonusMode: rofMulti = float(splitText[0]) else: @@ -614,7 +614,7 @@ def parseNeededFitDetails(fit, groupID): shipSize = shipSizes[7] else: shipSize = 'ShipSize not found for ' + fitName + ' groupID: ' + str(groupID) - print shipSize + print(shipSize) try: parsable = {'name': fitName, 'ehp': fit.ehp, 'droneDPS': fit.droneDPS, \ 'droneVolley': fit.droneVolley, 'hp': fit.hp, 'maxTargets': fit.maxTargets, \ @@ -634,13 +634,13 @@ def parseNeededFitDetails(fit, groupID): 'usingMWD': usingMWD, 'mwdPropSpeed': mwdPropSpeed } except TypeError: - print 'Error parsing fit:' + str(fit) - print TypeError + print('Error parsing fit:' + str(fit)) + print(TypeError) parsable = {'name': fitName + 'Fit could not be correctly parsed'} - #print fit.ship.itemModifiedAttributes.items() + #print(fit.ship.itemModifiedAttributes.items() #help(fit) #if len(fit.fighters) > 5: - #print fit.fighters + #print(fit.fighters #help(fit.fighters[0]) stringified = json.dumps(parsable, skipkeys=True) return stringified @@ -650,7 +650,7 @@ def setFitFromString(dnaString, fitName, groupID) : #if groupID == 485 and len(modArray) == 1: #additionalModeFit = ',\n' + setFitFromString(dnaString + ':4292', fitName + ' (Sieged)', groupID) fitL = Fit() - print modArray[0] + print(modArray[0]) fitID = fitL.newFit(int(modArray[0]), fitName) fit = eos.db.getFit(fitID) ammoArray = [] @@ -658,8 +658,8 @@ def setFitFromString(dnaString, fitName, groupID) : for mod in iter(modArray): n = n + 1 if n > 0: - #print n - #print mod + #print(n + #print(mod modSp = mod.split(';') if len(modSp) == 2: k = 0 @@ -682,7 +682,7 @@ def setFitFromString(dnaString, fitName, groupID) : fit = eos.db.getFit(fitID) #nonEmptyModules = fit.modules #while nonEmptyModules.find(None) >= 0: - # print 'ssssssssssssssss' + # print('ssssssssssssssss' # nonEmptyModules.remove(None) for ammo in iter(ammoArray): fitL.setAmmo(fitID, ammo, filter(lambda mod: str(mod).find('name') > 0, fit.modules)) @@ -697,7 +697,7 @@ def setFitFromString(dnaString, fitName, groupID) : abilityAltRef.active = True fitL.recalc(fit) fit = eos.db.getFit(fitID) - print filter(lambda mod: mod.item.groupID in [1189, 658], fit.modules) + print(filter(lambda mod: mod.item.groupID in [1189, 658], fit.modules)) #fit.calculateWeaponStats() fitL.addCommandFit(fit.ID, armorLinkShip) fitL.addCommandFit(fit.ID, shieldLinkShip) @@ -705,7 +705,7 @@ def setFitFromString(dnaString, fitName, groupID) : fitL.addCommandFit(fit.ID, infoLinkShip) #def anonfunc(unusedArg): True jsonStr = parseNeededFitDetails(fit, groupID) - #print vars(fit.ship._Ship__item) + #print(vars(fit.ship._Ship__item) #help(fit) Fit.deleteFit(fitID) return jsonStr + additionalModeFit From 56a3911b961e366ef4b23db98550f6283b89c96d Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Sat, 5 May 2018 04:09:01 -0400 Subject: [PATCH 09/49] Adjusted effs export to remove bugs with python3 --- effs_stat_export.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/effs_stat_export.py b/effs_stat_export.py index 91248276a..f45a4a65c 100755 --- a/effs_stat_export.py +++ b/effs_stat_export.py @@ -90,10 +90,6 @@ if not os.path.exists(config.savePath): eos.db.saveddata_meta.create_all() -#armorLinkShip = eos.db.searchFits('armor links')[0] -#infoLinkShip = eos.db.searchFits('information links')[0] -#shieldLinkShip = eos.db.searchFits('shield links')[0] -#skirmishLinkShip = eos.db.searchFits('skirmish links')[0] import json def processExportedHtml(fileLocation): @@ -138,7 +134,7 @@ def processExportedHtml(fileLocation): filter = dataTab.typeID == 638 except: filter = dataTab.ID == 638 - data = gamedata_session.query(dataTab).options().filter(filter).all() + data = gamedata_session.query(dataTab).options().list(filter(filter).all()) print(data) try: varDict = vars(data) @@ -158,7 +154,7 @@ def processExportedHtml(fileLocation): print('Not a list of dicts') #print(vars(shipCata._sa_instance_state)) - baseLimit = 0 + baseLimit = 10 baseN = 0 nameReqBase = ''; for ship in iter(shipCata): @@ -168,7 +164,7 @@ def processExportedHtml(fileLocation): outputBaseline.write(stats) outputBaseline.write(',\n') baseN += 1; - limit = 0 + limit = 100 skipTill = 0 nameReq = '' n = 0 @@ -240,7 +236,7 @@ def parseNeededFitDetails(fit, groupID): from eos.db import gamedata_session from eos.gamedata import Group, Category filterVal = Group.categoryID == 6 - data = gamedata_session.query(Group).options().filter(filterVal).all() + data = gamedata_session.query(Group).options().list(filter(filterVal).all()) for group in data: print(group.groupName + ' groupID: ' + str(group.groupID)) #print(group.categoryName + ' categoryID: ' + str(group.categoryID) + ', published: ' + str(group.published) @@ -251,13 +247,13 @@ def parseNeededFitDetails(fit, groupID): 41, 52, 65, 67, 68, 71, 80, 201, 208, 291, 325, 379, 585, 842, 899, 1150, 1154, 1189, 1306, 1672, 1697, 1698, 1815, 1894 ] - projectedMods = filter(lambda mod: mod.item and mod.item.groupID in projectedModGroupIds, fit.modules) + projectedMods = list(filter(lambda mod: mod.item and mod.item.groupID in projectedModGroupIds, fit.modules)) unpropedSpeed = fit.maxSpeed unpropedSig = fit.ship.itemModifiedAttributes['signatureRadius'] usingMWD = False - propMods = filter(lambda mod: mod.item and mod.item.groupID in [46], fit.modules) - possibleMWD = filter(lambda mod: 'signatureRadiusBonus' in mod.item.attributes, propMods) + propMods = list(filter(lambda mod: mod.item and mod.item.groupID in [46], fit.modules)) + possibleMWD = list(filter(lambda mod: 'signatureRadiusBonus' in mod.item.attributes, propMods)) if len(possibleMWD) > 0 and possibleMWD[0].state > 0: mwd = possibleMWD[0] oldMwdState = mwd.state @@ -295,7 +291,7 @@ def parseNeededFitDetails(fit, groupID): fitL.recalc(fit) fit = eos.db.getFit(fitID) mwdPropSpeed = fit.maxSpeed - mwdPosition = filter(lambda mod: mod.item and mod.item.ID == propID, fit.modules)[0].position + mwdPosition = list(filter(lambda mod: mod.item and mod.item.ID == propID, fit.modules))[0].position fitL.removeModule(fitID, mwdPosition) fitL.recalc(fit) fit = eos.db.getFit(fitID) @@ -644,7 +640,22 @@ def parseNeededFitDetails(fit, groupID): #help(fit.fighters[0]) stringified = json.dumps(parsable, skipkeys=True) return stringified + +try: + armorLinkShip = eos.db.searchFits('armor links')[0] + infoLinkShip = eos.db.searchFits('information links')[0] + shieldLinkShip = eos.db.searchFits('shield links')[0] + skirmishLinkShip = eos.db.searchFits('skirmish links')[0] +except: + armorLinkShip = None + infoLinkShip = None + shieldLinkShip = None + skirmishLinkShip = None + def setFitFromString(dnaString, fitName, groupID) : + if armorLinkShip == None: + print('Cannot find correct link fits for base calculations') + return '' modArray = dnaString.split(':') additionalModeFit = '' #if groupID == 485 and len(modArray) == 1: @@ -685,7 +696,7 @@ def setFitFromString(dnaString, fitName, groupID) : # print('ssssssssssssssss' # nonEmptyModules.remove(None) for ammo in iter(ammoArray): - fitL.setAmmo(fitID, ammo, filter(lambda mod: str(mod).find('name') > 0, fit.modules)) + fitL.setAmmo(fitID, ammo, list(filter(lambda mod: str(mod).find('name') > 0, fit.modules))) if len(fit.drones) > 0: fit.drones[0].amountActive = fit.drones[0].amount eos.db.commit() @@ -697,7 +708,7 @@ def setFitFromString(dnaString, fitName, groupID) : abilityAltRef.active = True fitL.recalc(fit) fit = eos.db.getFit(fitID) - print(filter(lambda mod: mod.item.groupID in [1189, 658], fit.modules)) + print(list(filter(lambda mod: mod.item.groupID in [1189, 658], fit.modules))) #fit.calculateWeaponStats() fitL.addCommandFit(fit.ID, armorLinkShip) fitL.addCommandFit(fit.ID, shieldLinkShip) From 2a410a13a0567cf1ae4d2ce7793fd79767a74193 Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Sun, 20 May 2018 04:36:20 -0400 Subject: [PATCH 10/49] EFS mass export now uses the configured save path for consistancy. --- effs_stat_export.py | 32 ++++++++++++-------------------- shipBaseJSON.js | 2 -- shipJSON.js | 2 -- 3 files changed, 12 insertions(+), 24 deletions(-) delete mode 100644 shipBaseJSON.js delete mode 100644 shipJSON.js diff --git a/effs_stat_export.py b/effs_stat_export.py index f45a4a65c..0b3836b93 100755 --- a/effs_stat_export.py +++ b/effs_stat_export.py @@ -92,10 +92,11 @@ eos.db.saveddata_meta.create_all() import json -def processExportedHtml(fileLocation): - output = open('./shipJSON.js', 'w') +def processExportedHtml(): + basePath = config.savePath + os.sep + output = open(basePath + 'shipJSON.js', 'w') output.write('let shipJSON = JSON.stringify([') - outputBaseline = open('./shipBaseJSON.js', 'w') + outputBaseline = open(basePath + 'shipBaseJSON.js', 'w') outputBaseline.write('let shipBaseJSON = JSON.stringify([') shipCata = eos.db.getItemsByCategory('Ship') #shipCata = eos.db.getItem(638) @@ -154,7 +155,7 @@ def processExportedHtml(fileLocation): print('Not a list of dicts') #print(vars(shipCata._sa_instance_state)) - baseLimit = 10 + baseLimit = 0 baseN = 0 nameReqBase = ''; for ship in iter(shipCata): @@ -164,7 +165,7 @@ def processExportedHtml(fileLocation): outputBaseline.write(stats) outputBaseline.write(',\n') baseN += 1; - limit = 100 + limit = 10 skipTill = 0 nameReq = '' n = 0 @@ -173,18 +174,12 @@ def processExportedHtml(fileLocation): fileLocation = 'pyfaFits.html' except: try: - with open('.pyfa/pyfaFits.html'): - fileLocation = '.pyfa/pyfaFits.html' + d = config.savePath + os.sep + 'pyfaFits.html' + print(d) + with open(d): + fileLocation = d except: - try: - with open('../.pyfa/pyfaFits.html'): - fileLocation = '../.pyfa/pyfaFits.html' - except: - try: - with open('../../.pyfa/pyfaFits.html'): - fileLocation = '../../.pyfa/pyfaFits.html' - except: - fileLocation = None; + fileLocation = None; fitList = eos.db.getFitList() with open(fileLocation) as f: for fit in fitList: @@ -724,7 +719,4 @@ launchUI = False #launchUI = True if launchUI == False: from service.fit import Fit - #setFitFromString(dnaChim, 'moMachsD') - #help(eos.db.getItem) - #ship = es_Ship(eos.db.getItem(27)) - processExportedHtml('../.pyfa/pyfaFits.html') + processExportedHtml() diff --git a/shipBaseJSON.js b/shipBaseJSON.js deleted file mode 100644 index 28b7228b2..000000000 --- a/shipBaseJSON.js +++ /dev/null @@ -1,2 +0,0 @@ -let shipBaseJSON = JSON.stringify([]); -export {shipBaseJSON}; \ No newline at end of file diff --git a/shipJSON.js b/shipJSON.js deleted file mode 100644 index 22c2ed09f..000000000 --- a/shipJSON.js +++ /dev/null @@ -1,2 +0,0 @@ -let shipJSON = JSON.stringify([]); -export {shipJSON}; \ No newline at end of file From 05e76a884ad879516feb308a82944f292b97139b Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Sun, 27 May 2018 04:07:21 -0400 Subject: [PATCH 11/49] Partially cleaned up efs utilities --- effs_stat_export.py | 672 ++++++++------------------- savedata/effs_export_all_fits.py | 0 savedata/effs_export_base_fits.py | 247 ++++++++++ savedata/effs_export_pyfa_fits.py | 53 +++ savedata/effs_process_html_export.py | 60 +++ savedata/effs_util.py | 56 +++ 6 files changed, 614 insertions(+), 474 deletions(-) create mode 100644 savedata/effs_export_all_fits.py create mode 100644 savedata/effs_export_base_fits.py create mode 100644 savedata/effs_export_pyfa_fits.py create mode 100644 savedata/effs_process_html_export.py create mode 100644 savedata/effs_util.py diff --git a/effs_stat_export.py b/effs_stat_export.py index 0b3836b93..c89d0f781 100755 --- a/effs_stat_export.py +++ b/effs_stat_export.py @@ -4,249 +4,49 @@ import platform import re import sys import traceback -from optparse import AmbiguousOptionError, BadOptionError, OptionParser - -from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, StreamHandler, TimedRotatingFileHandler, WARNING, \ - __version__ as logbook_version - -import config - from math import log -try: - import wxversion -except ImportError: - wxversion = None - -try: - import sqlalchemy -except ImportError: - sqlalchemy = None - -pyfalog = Logger(__name__) - -class PassThroughOptionParser(OptionParser): - - def _process_args(self, largs, rargs, values): - while rargs: - try: - OptionParser._process_args(self, largs, rargs, values) - except (BadOptionError, AmbiguousOptionError) as e: - pyfalog.error("Bad startup option passed.") - largs.append(e.opt_str) - -usage = "usage: %prog [--root]" -parser = PassThroughOptionParser(usage=usage) -parser.add_option("-r", "--root", action="store_true", dest="rootsavedata", help="if you want pyfa to store its data in root folder, use this option", default=False) -parser.add_option("-w", "--wx28", action="store_true", dest="force28", help="Force usage of wxPython 2.8", default=False) -parser.add_option("-d", "--debug", action="store_true", dest="debug", help="Set logger to debug level.", default=False) -parser.add_option("-t", "--title", action="store", dest="title", help="Set Window Title", default=None) -parser.add_option("-s", "--savepath", action="store", dest="savepath", help="Set the folder for savedata", default=None) -parser.add_option("-l", "--logginglevel", action="store", dest="logginglevel", help="Set desired logging level [Critical|Error|Warning|Info|Debug]", default="Error") - -(options, args) = parser.parse_args() - -if options.rootsavedata is True: - config.saveInRoot = True - -# set title if it wasn't supplied by argument -if options.title is None: - options.title = "pyfa %s%s - Python Fitting Assistant" % (config.version, "" if config.tag.lower() != 'git' else " (git)") - -config.debug = options.debug - -# convert to unicode if it is set -if options.savepath is not None: - options.savepath = unicode(options.savepath) -config.defPaths(options.savepath) - -try: - # noinspection PyPackageRequirements - import wx -except: - exit_message = "Cannot import wxPython. You can download wxPython (2.8+) from http://www.wxpython.org/" - raise PreCheckException(exit_message) - -try: - import requests - config.requestsVersion = requests.__version__ -except ImportError: - raise PreCheckException("Cannot import requests. You can download requests from https://pypi.python.org/pypi/requests.") - import eos.db -#if config.saVersion[0] > 0 or config.saVersion[1] >= 7: - # <0.7 doesn't have support for events ;_; (mac-deprecated) -config.sa_events = True -import eos.events - - # noinspection PyUnresolvedReferences -import service.prefetch # noqa: F401 - - # Make sure the saveddata db exists -if not os.path.exists(config.savePath): - os.mkdir(config.savePath) - eos.db.saveddata_meta.create_all() - import json +from service.fit import Fit -def processExportedHtml(): - basePath = config.savePath + os.sep - output = open(basePath + 'shipJSON.js', 'w') - output.write('let shipJSON = JSON.stringify([') - outputBaseline = open(basePath + 'shipBaseJSON.js', 'w') - outputBaseline.write('let shipBaseJSON = JSON.stringify([') - shipCata = eos.db.getItemsByCategory('Ship') - #shipCata = eos.db.getItem(638) - #shipCata = eos.db.getMetaGroup(638) - #shipCata = eos.db.getAttributeInfo(638) - #shipCata = eos.db.getItemsByCategory('Traits') - #shipCata = eos.db.getGroup('invtraits') - #shipCata = eos.db.getCategory('Traits') - from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, Table - from sqlalchemy.orm import relation, mapper, synonym, deferred - from eos.db import gamedata_session - from eos.db import gamedata_meta - from eos.db.gamedata.metaGroup import metatypes_table, items_table - from eos.db.gamedata.group import groups_table - - from eos.gamedata import AlphaClone, Attribute, Category, Group, Item, MarketGroup, \ - MetaGroup, AttributeInfo, MetaData, Effect, ItemEffect, Traits - from eos.db.gamedata.traits import traits_table - #shipCata = traits_table #categories_table - #print shipCata - #print shipCata.columns - #print shipCata.categoryName - #print vars(shipCata) - data = category = gamedata_session.query(Category).all() - #print data - #print iter(data) - eff = gamedata_session.query(Category).get(53) #Bonus (id14) #Effects (id 53) - data = eff; - #print eff - #print vars(eff) - things = []#[Category, MetaGroup, AttributeInfo, MetaData, Item, Attribute, Effect, ItemEffect, Traits]#, Attribute] - if False: - for dataTab in things : - print('Data for: ' + str(dataTab)) - try: - filter = dataTab.typeID == 638 - except: - filter = dataTab.ID == 638 - data = gamedata_session.query(dataTab).options().list(filter(filter).all()) - print(data) - try: - varDict = vars(data) - print(varDict) - except: - print('Not a Dict') - try: - varDict = data.__doc__ - print(varDict) - except: - print('No items()') - try: - for varDict in data: - print(varDict) - print(vars(varDict)) - except: - print('Not a list of dicts') - - #print(vars(shipCata._sa_instance_state)) - baseLimit = 0 - baseN = 0 - nameReqBase = ''; - for ship in iter(shipCata): - if baseN < baseLimit and nameReqBase in ship.name: - dna = str(ship.ID) - stats = setFitFromString(dna, ship.name, ship.groupID) - outputBaseline.write(stats) - outputBaseline.write(',\n') - baseN += 1; - limit = 10 - skipTill = 0 - nameReq = '' - n = 0 - try: - with open('pyfaFits.html'): - fileLocation = 'pyfaFits.html' - except: - try: - d = config.savePath + os.sep + 'pyfaFits.html' - print(d) - with open(d): - fileLocation = d - except: - fileLocation = None; - fitList = eos.db.getFitList() - with open(fileLocation) as f: - for fit in fitList: - if limit == None or n < limit: - n += 1 - name = fit.ship.name + ': ' + fit.name - if n >= skipTill and nameReq in name: - stats = parseNeededFitDetails(fit, 0) - output.write(stats) - output.write(',\n') - if False and fileLocation != None: - with open(fileLocation) as f: - for fullLine in f: - if limit == None or n < limit: - n += 1 - startInd = fullLine.find('/dna/') + 5 - line = fullLine[startInd:len(fullLine)] - endInd = line.find('::') - dna = line[0:endInd] - name = line[line.find('>') + 1:line.find('<')] - if n >= skipTill and nameReq in name: - print('name: ' + name + ' DNA: ' + dna) - stats = setFitFromString(dna, name, 0) - output.write(stats) - output.write(',\n') - output.write(']);\nexport {shipJSON};') - output.close() - outputBaseline.write(']);\nexport {shipBaseJSON};') - outputBaseline.close() def attrDirectMap(values, target, source): for val in values: target[val] = source.itemModifiedAttributes[val] -def parseNeededFitDetails(fit, groupID): - singleRunPrintPreformed = False - weaponSystems = [] - groups = {} - moduleNames = [] - fitID = fit.ID - if len(fit.modules) > 0: - fitName = fit.ship.name + ': ' + fit.name - else: - fitName = fit.name - print('') - print('name: ' + fit.name) - fitL = Fit() - fitL.recalc(fit) - fit = eos.db.getFit(fitID) - if False: - from eos.db import gamedata_session - from eos.gamedata import Group, Category - filterVal = Group.categoryID == 6 - data = gamedata_session.query(Group).options().list(filter(filterVal).all()) - for group in data: - print(group.groupName + ' groupID: ' + str(group.groupID)) - #print(group.categoryName + ' categoryID: ' + str(group.categoryID) + ', published: ' + str(group.published) - #print(vars(group) - #print('' - return '' - projectedModGroupIds = [ - 41, 52, 65, 67, 68, 71, 80, 201, 208, 291, 325, 379, 585, - 842, 899, 1150, 1154, 1189, 1306, 1672, 1697, 1698, 1815, 1894 - ] - projectedMods = list(filter(lambda mod: mod.item and mod.item.groupID in projectedModGroupIds, fit.modules)) - unpropedSpeed = fit.maxSpeed - unpropedSig = fit.ship.itemModifiedAttributes['signatureRadius'] - usingMWD = False +def getT2MwdSpeed(fit, fitL): + fitID = fit.ID + propID = None + rigSize = fit.ship.itemModifiedAttributes['rigSize'] + if rigSize == 1 and fit.ship.itemModifiedAttributes['medSlots'] > 0: + propID = 440 + elif rigSize == 2 and fit.ship.itemModifiedAttributes['medSlots'] > 0: + propID = 12076 + elif rigSize == 3 and fit.ship.itemModifiedAttributes['medSlots'] > 0: + propID = 12084 + elif rigSize == 4 and fit.ship.itemModifiedAttributes['medSlots'] > 0: + if fit.ship.itemModifiedAttributes['powerOutput'] > 60000: + propID = 41253 + else: + propID = 12084 + elif rigSize == None and fit.ship.itemModifiedAttributes['medSlots'] > 0: + propID = 440 + if propID: + fitL.appendModule(fitID, propID) + fitL.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 + fitL.removeModule(fitID, mwdPosition) + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + return mwdPropSpeed + +def getPropData(fit, fitL): + fitID = fit.ID propMods = list(filter(lambda mod: mod.item and mod.item.groupID in [46], fit.modules)) possibleMWD = list(filter(lambda mod: 'signatureRadiusBonus' in mod.item.attributes, propMods)) if len(possibleMWD) > 0 and possibleMWD[0].state > 0: @@ -255,42 +55,21 @@ def parseNeededFitDetails(fit, groupID): mwd.state = 0 fitL.recalc(fit) fit = eos.db.getFit(fitID) - unpropedSpeed = fit.maxSpeed - unpropedSig = fit.ship.itemModifiedAttributes['signatureRadius'] + sp = fit.maxSpeed + sig = fit.ship.itemModifiedAttributes['signatureRadius'] mwd.state = oldMwdState fitL.recalc(fit) fit = eos.db.getFit(fitID) - usingMWD = True - - print(fit.ship.itemModifiedAttributes['rigSize']) - print(propMods) - mwdPropSpeed = fit.maxSpeed - if groupID > 0: - propID = None - rigSize = fit.ship.itemModifiedAttributes['rigSize'] - if rigSize == 1 and fit.ship.itemModifiedAttributes['medSlots'] > 0: - propID = 440 - elif rigSize == 2 and fit.ship.itemModifiedAttributes['medSlots'] > 0: - propID = 12076 - elif rigSize == 3 and fit.ship.itemModifiedAttributes['medSlots'] > 0: - propID = 12084 - elif rigSize == 4 and fit.ship.itemModifiedAttributes['medSlots'] > 0: - if fit.ship.itemModifiedAttributes['powerOutput'] > 60000: - propID = 41253 - else: - propID = 12084 - elif rigSize == None and fit.ship.itemModifiedAttributes['medSlots'] > 0: - propID = 440 - if propID: - fitL.appendModule(fitID, propID) - fitL.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 - fitL.removeModule(fitID, mwdPosition) - fitL.recalc(fit) - fit = eos.db.getFit(fitID) + return {'usingMWD': True, 'unpropedSpeed': sp, 'unpropedSig': sig} + return {'usingMWD': False, 'unpropedSpeed': fit.maxSpeed, 'unpropedSig': fit.ship.itemModifiedAttributes['signatureRadius']} +def getOutgoingProjectionData(fit): + # This is a subset of module groups capable of projection and a superset of those currently used by efs + projectedModGroupIds = [ + 41, 52, 65, 67, 68, 71, 80, 201, 208, 291, 325, 379, 585, + 842, 899, 1150, 1154, 1189, 1306, 1672, 1697, 1698, 1815, 1894 + ] + projectedMods = list(filter(lambda mod: mod.item and mod.item.groupID in projectedModGroupIds, fit.modules)) projections = [] for mod in projectedMods: stats = {} @@ -356,15 +135,10 @@ def parseNeededFitDetails(fit, groupID): stats['falloff'] = mod.itemModifiedAttributes['falloffEffectiveness'] or 0 attrDirectMap(['duration', 'capacitorNeed'], stats, mod) projections.append(stats) - #print('' - #print(stats - #print(mod.item.name - #print(mod.itemModifiedAttributes.items() - #print('' - #print(vars(mod.item) - #print(vars(web.itemModifiedAttributes) - #print(vars(fit.modules) - #print(vars(fit.modules[0]) + return projections + +def getModuleNames(fit): + moduleNames = [] highSlotNames = [] midSlotNames = [] lowSlotNames = [] @@ -393,12 +167,6 @@ def parseNeededFitDetails(fit, groupID): print(vars(mod)) print('could not find name for module') print(fit.modules) - if mod.dps > 0: - keystr = str(mod.itemID) + '-' + str(mod.chargeID) - if keystr in groups: - groups[keystr][1] += 1 - else: - groups[keystr] = [mod, 1] for modInfo in [['High Slots:'], highSlotNames, ['', 'Med Slots:'], midSlotNames, ['', 'Low Slots:'], lowSlotNames, ['', 'Rig Slots:'], rigSlotNames]: moduleNames.extend(modInfo) if len(miscSlotNames) > 0: @@ -409,10 +177,10 @@ def parseNeededFitDetails(fit, groupID): fighterNames = [] for drone in fit.drones: if drone.amountActive > 0: - droneNames.append(drone.item.name) + droneNames.append("%s x%s" % (drone.item.name, drone.amount)) for fighter in fit.fighters: if fighter.amountActive > 0: - fighterNames.append(fighter.item.name) + fighterNames.append("%s x%s" % (fighter.item.name, fighter.amountActive)) if len(droneNames) > 0: moduleNames.append('') moduleNames.append('Drones:') @@ -431,7 +199,18 @@ def parseNeededFitDetails(fit, groupID): moduleNames.append('Command Fits:') for commandFit in fit.commandFits: moduleNames.append(commandFit.name) + return moduleNames +def getWeaponSystemData(fit): + weaponSystems = [] + groups = {} + for mod in fit.modules: + if mod.dps > 0: + keystr = str(mod.itemID) + '-' + str(mod.chargeID) + if keystr in groups: + groups[keystr][1] += 1 + else: + groups[keystr] = [mod, 1] for wepGroup in groups: stats = groups[wepGroup][0] c = groups[wepGroup][1] @@ -507,216 +286,161 @@ def parseNeededFitDetails(fit, groupID): 'volley': fighter.dps[1], 'signatureRadius': fighter.itemModifiedAttributes['signatureRadius']\ } weaponSystems.append(statDict) - turretSlots = fit.ship.itemModifiedAttributes['turretSlotsLeft'] - launcherSlots = fit.ship.itemModifiedAttributes['launcherSlotsLeft'] - droneBandwidth = fit.ship.itemModifiedAttributes['droneBandwidth'] - if turretSlots == None: - turretSlots = 0 - if launcherSlots == None: - launcherSlots = 0 - if droneBandwidth == None: - droneBandwidth = 0 - effectiveTurretSlots = turretSlots - effectiveLauncherSlots = launcherSlots - effectiveDroneBandwidth = droneBandwidth + return weaponSystems + +def getWeaponBonusMultipliers(fit): + multipliers = {'turret': 1, 'launcher': 1, 'droneBandwidth': 1} from eos.db import gamedata_session from eos.gamedata import Traits filterVal = Traits.typeID == fit.shipID data = gamedata_session.query(Traits).options().filter(filterVal).all() roleBonusMode = False - if len(data) != 0: - #print(data[0].traitText - previousTypedBonus = 0 - previousDroneTypeBonus = 0 - for bonusText in data[0].traitText.splitlines(): - bonusText = bonusText.lower() - #print('bonus text line: ' + bonusText - if 'per skill level' in bonusText: - roleBonusMode = False - if 'role bonus' in bonusText or 'misc bonus' in bonusText: - roleBonusMode = True - multi = 1 - if 'damage' in bonusText and not any(e in bonusText for e in ['control', 'heat']):#'control' in bonusText and not 'heat' in bonusText: - splitText = bonusText.split('%') - if (float(splitText[0]) > 0) == False: - print('damage bonus split did not parse correctly!') - print(float(splitText[0])) - if roleBonusMode: - addedMulti = float(splitText[0]) + if len(data) == 0: + return multipliers + previousTypedBonus = 0 + previousDroneTypeBonus = 0 + for bonusText in data[0].traitText.splitlines(): + bonusText = bonusText.lower() + if 'per skill level' in bonusText: + roleBonusMode = False + if 'role bonus' in bonusText or 'misc bonus' in bonusText: + roleBonusMode = True + multi = 1 + if 'damage' in bonusText and not any(e in bonusText for e in ['control', 'heat']): + splitText = bonusText.split('%') + if (float(splitText[0]) > 0) == False: + print('damage bonus split did not parse correctly!') + print(float(splitText[0])) + if roleBonusMode: + addedMulti = float(splitText[0]) + else: + addedMulti = float(splitText[0]) * 5 + if any(e in bonusText for e in [' em', 'thermal', 'kinetic', 'explosive']): + if addedMulti > previousTypedBonus: + previousTypedBonus = addedMulti else: - addedMulti = float(splitText[0]) * 5 - if any(e in bonusText for e in [' em', 'thermal', 'kinetic', 'explosive']): - if addedMulti > previousTypedBonus: - previousTypedBonus = addedMulti - else: - addedMulti = 0 - if any(e in bonusText for e in ['heavy drone', 'medium drone', 'light drone', 'sentry drone']): - if addedMulti > previousDroneTypeBonus: - previousDroneTypeBonus = addedMulti - else: - addedMulti = 0 - multi = 1 + (addedMulti / 100) - elif 'rate of fire' in bonusText: - splitText = bonusText.split('%') - if (float(splitText[0]) > 0) == False: - print('rate of fire bonus split did not parse correctly!') - print(float(splitText[0])) - if roleBonusMode: - rofMulti = float(splitText[0]) + addedMulti = 0 + if any(e in bonusText for e in ['heavy drone', 'medium drone', 'light drone', 'sentry drone']): + if addedMulti > previousDroneTypeBonus: + previousDroneTypeBonus = addedMulti else: - rofMulti = float(splitText[0]) * 5 - multi = 1 / (1 - (rofMulti / 100)) - if multi > 1: - if 'drone' in bonusText.lower(): - effectiveDroneBandwidth *= multi - elif 'turret' in bonusText.lower(): - effectiveTurretSlots *= multi - elif any(e in bonusText for e in ['missile', 'torpedo']): - effectiveLauncherSlots *= multi + addedMulti = 0 + multi = 1 + (addedMulti / 100) + elif 'rate of fire' in bonusText: + splitText = bonusText.split('%') + if (float(splitText[0]) > 0) == False: + print('rate of fire bonus split did not parse correctly!') + print(float(splitText[0])) + if roleBonusMode: + rofMulti = float(splitText[0]) + else: + rofMulti = float(splitText[0]) * 5 + multi = 1 / (1 - (rofMulti / 100)) + if multi > 1: + if 'drone' in bonusText.lower(): + multipliers['droneBandwidth'] *= multi + elif 'turret' in bonusText.lower(): + multipliers['turret'] *= multi + elif any(e in bonusText for e in ['missile', 'torpedo']): + multipliers['launcher'] *= multi + return multipliers +def getShipSize(groupID): + # Sizings are somewhat arbitrary but allow for a more managable number of top level groupings in a tree structure. + shipSizes = ['Frigate', 'Destroyer', 'Cruiser', 'Battlecruiser', 'Battleship', 'Capital', 'Industrial', 'Misc'] + if groupID in [25, 31, 237, 324, 830, 831, 834, 893, 1283, 1527]: + return shipSizes[0] + elif groupID in [420, 541, 1305, 1534]: + return shipSizes[1] + elif groupID in [26, 358, 832, 833, 894, 906, 963]: + return shipSizes[2] + elif groupID in [419, 540, 1201]: + return shipSizes[3] + elif groupID in [27, 381, 898, 900]: + return shipSizes[4] + elif groupID in [30, 485, 513, 547, 659, 883, 902, 1538]: + return shipSizes[5] + elif groupID in [28, 380, 1202, 463, 543, 941]: + return shipSizes[6] + elif groupID in [29, 1022]: + return shipSizes[7] + else: + sizeNotFoundMsg = 'ShipSize not found for groupID: ' + str(groupID) + print(sizeNotFoundMsg) + return sizeNotFoundMsg + +def parseNeededFitDetails(fit, groupID): + includeShipTypeData = groupID > 0 + fitID = fit.ID + if len(fit.modules) > 0: + fitName = fit.ship.name + ': ' + fit.name + else: + fitName = fit.name + print('') + print('name: ' + fit.name) + fitL = Fit() + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + fitModAttr = fit.ship.itemModifiedAttributes + propData = getPropData(fit, fitL) + print(fitModAttr['rigSize']) + print(propData) + mwdPropSpeed = fit.maxSpeed + if includeShipTypeData: + mwdPropSpeed = getT2MwdSpeed(fit, fitL) + projections = getOutgoingProjectionData(fit) + moduleNames = getModuleNames(fit) + weaponSystems = 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 = 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 groupID == 485: effectiveTurretSlots *= 9.4 effectiveLauncherSlots *= 15 - effectiveTurretSlots = round(effectiveTurretSlots, 2); - effectiveLauncherSlots = round(effectiveLauncherSlots, 2); - effectiveDroneBandwidth = round(effectiveDroneBandwidth, 2); - hullResonance = {'exp': fit.ship.itemModifiedAttributes['explosiveDamageResonance'], 'kin': fit.ship.itemModifiedAttributes['kineticDamageResonance'], \ - 'therm': fit.ship.itemModifiedAttributes['thermalDamageResonance'], 'em': fit.ship.itemModifiedAttributes['emDamageResonance']} - armorResonance = {'exp': fit.ship.itemModifiedAttributes['armorExplosiveDamageResonance'], 'kin': fit.ship.itemModifiedAttributes['armorKineticDamageResonance'], \ - 'therm': fit.ship.itemModifiedAttributes['armorThermalDamageResonance'], 'em': fit.ship.itemModifiedAttributes['armorEmDamageResonance']} - shieldResonance = {'exp': fit.ship.itemModifiedAttributes['shieldExplosiveDamageResonance'], 'kin': fit.ship.itemModifiedAttributes['shieldKineticDamageResonance'], \ - 'therm': fit.ship.itemModifiedAttributes['shieldThermalDamageResonance'], 'em': fit.ship.itemModifiedAttributes['shieldEmDamageResonance']} + 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} - shipSizes = ['Frigate', 'Destroyer', 'Cruiser', 'Battlecruiser', 'Battleship', 'Capital', 'Industrial', 'Misc'] - if groupID in [25, 31, 237, 324, 830, 831, 834, 893, 1283, 1527]: - shipSize = shipSizes[0] - elif groupID in [420, 541, 1305, 1534]: - shipSize = shipSizes[1] - elif groupID in [26, 358, 832, 833, 894, 906, 963]: - shipSize = shipSizes[2] - elif groupID in [419, 540, 1201]: - shipSize = shipSizes[3] - elif groupID in [27, 381, 898, 900]: - shipSize = shipSizes[4] - elif groupID in [30, 485, 513, 547, 659, 883, 902, 1538]: - shipSize = shipSizes[5] - elif groupID in [28, 380, 1202, 463, 543, 941]: - shipSize = shipSizes[6] - elif groupID in [29, 1022]: - shipSize = shipSizes[7] - else: - shipSize = 'ShipSize not found for ' + fitName + ' groupID: ' + str(groupID) - print(shipSize) + shipSize = getShipSize(groupID) + try: - parsable = {'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': fit.ship.itemModifiedAttributes['signatureRadius'],\ - 'weapons': weaponSystems, 'scanRes': fit.ship.itemModifiedAttributes['scanResolution'],\ - 'projectedModules': fit.projectedModules, 'capUsed': fit.capUsed, 'capRecharge': fit.capRecharge,\ - 'rigSlots': fit.ship.itemModifiedAttributes['rigSlots'], 'lowSlots': fit.ship.itemModifiedAttributes['lowSlots'],\ - 'midSlots': fit.ship.itemModifiedAttributes['medSlots'], 'highSlots': fit.ship.itemModifiedAttributes['hiSlots'],\ - 'turretSlots': fit.ship.itemModifiedAttributes['turretSlotsLeft'], 'launcherSlots': fit.ship.itemModifiedAttributes['launcherSlotsLeft'],\ - 'powerOutput': fit.ship.itemModifiedAttributes['powerOutput'], 'rigSize': fit.ship.itemModifiedAttributes['rigSize'],\ - 'effectiveTurrets': effectiveTurretSlots, 'effectiveLaunchers': effectiveLauncherSlots, 'effectiveDroneBandwidth': effectiveDroneBandwidth,\ - 'resonance': resonance, 'typeID': fit.shipID, 'groupID': groupID, 'shipSize': shipSize,\ - 'droneControlRange': fit.ship.itemModifiedAttributes['droneControlRange'], 'mass': fit.ship.itemModifiedAttributes['mass'],\ - 'moduleNames': moduleNames, 'projections': projections, 'unpropedSpeed': unpropedSpeed, 'unpropedSig': unpropedSig,\ - 'usingMWD': usingMWD, 'mwdPropSpeed': mwdPropSpeed + parsable = { + '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'],\ + 'projectedModules': fit.projectedModules, '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'], 'rigSize': fitModAttr['rigSize'],\ + 'effectiveTurrets': effectiveTurretSlots, 'effectiveLaunchers': effectiveLauncherSlots,\ + 'effectiveDroneBandwidth': effectiveDroneBandwidth,\ + 'resonance': resonance, 'typeID': fit.shipID, 'groupID': groupID, 'shipSize': shipSize,\ + 'droneControlRange': fitModAttr['droneControlRange'], 'mass': fitModAttr['mass'],\ + 'moduleNames': moduleNames, 'projections': projections,\ + 'unpropedSpeed': propData['unpropedSpeed'], 'unpropedSig': propData['unpropedSig'],\ + 'usingMWD': propData['usingMWD'], 'mwdPropSpeed': mwdPropSpeed } except TypeError: print('Error parsing fit:' + str(fit)) print(TypeError) parsable = {'name': fitName + 'Fit could not be correctly parsed'} - #print(fit.ship.itemModifiedAttributes.items() - #help(fit) - #if len(fit.fighters) > 5: - #print(fit.fighters - #help(fit.fighters[0]) stringified = json.dumps(parsable, skipkeys=True) return stringified - -try: - armorLinkShip = eos.db.searchFits('armor links')[0] - infoLinkShip = eos.db.searchFits('information links')[0] - shieldLinkShip = eos.db.searchFits('shield links')[0] - skirmishLinkShip = eos.db.searchFits('skirmish links')[0] -except: - armorLinkShip = None - infoLinkShip = None - shieldLinkShip = None - skirmishLinkShip = None - -def setFitFromString(dnaString, fitName, groupID) : - if armorLinkShip == None: - print('Cannot find correct link fits for base calculations') - return '' - modArray = dnaString.split(':') - additionalModeFit = '' - #if groupID == 485 and len(modArray) == 1: - #additionalModeFit = ',\n' + setFitFromString(dnaString + ':4292', fitName + ' (Sieged)', groupID) - fitL = Fit() - print(modArray[0]) - fitID = fitL.newFit(int(modArray[0]), fitName) - fit = eos.db.getFit(fitID) - ammoArray = [] - n = -1 - for mod in iter(modArray): - n = n + 1 - if n > 0: - #print(n - #print(mod - modSp = mod.split(';') - if len(modSp) == 2: - k = 0 - while k < int(modSp[1]): - k = k + 1 - itemID = int(modSp[0]) - item = eos.db.getItem(int(modSp[0]), eager=("attributes", "group.category")) - cat = item.category.name - if cat == 'Drone': - fitL.addDrone(fitID, itemID, int(modSp[1]), recalc=False) - k += int(modSp[1]) - if cat == 'Fighter': - fitL.addFighter(fitID, itemID, recalc=False) - #fit.fighters.last.abilities.active = True - k += 100 - if fitL.isAmmo(int(modSp[0])): - k += 100 - ammoArray.append(int(modSp[0])); - fitL.appendModule(fitID, int(modSp[0])) - fit = eos.db.getFit(fitID) - #nonEmptyModules = fit.modules - #while nonEmptyModules.find(None) >= 0: - # print('ssssssssssssssss' - # nonEmptyModules.remove(None) - for ammo in iter(ammoArray): - fitL.setAmmo(fitID, ammo, list(filter(lambda mod: str(mod).find('name') > 0, fit.modules))) - if len(fit.drones) > 0: - fit.drones[0].amountActive = fit.drones[0].amount - eos.db.commit() - for fighter in iter(fit.fighters): - for ability in fighter.abilities: - if ability.effect.handlerName == u'fighterabilityattackm' and ability.active == True: - for abilityAltRef in fighter.abilities: - if abilityAltRef.effect.isImplemented: - abilityAltRef.active = True - fitL.recalc(fit) - fit = eos.db.getFit(fitID) - print(list(filter(lambda mod: mod.item.groupID in [1189, 658], fit.modules))) - #fit.calculateWeaponStats() - fitL.addCommandFit(fit.ID, armorLinkShip) - fitL.addCommandFit(fit.ID, shieldLinkShip) - fitL.addCommandFit(fit.ID, skirmishLinkShip) - fitL.addCommandFit(fit.ID, infoLinkShip) - #def anonfunc(unusedArg): True - jsonStr = parseNeededFitDetails(fit, groupID) - #print(vars(fit.ship._Ship__item) - #help(fit) - Fit.deleteFit(fitID) - return jsonStr + additionalModeFit -launchUI = False -#launchUI = True -if launchUI == False: - from service.fit import Fit - processExportedHtml() diff --git a/savedata/effs_export_all_fits.py b/savedata/effs_export_all_fits.py new file mode 100644 index 000000000..e69de29bb diff --git a/savedata/effs_export_base_fits.py b/savedata/effs_export_base_fits.py new file mode 100644 index 000000000..e1be23e3b --- /dev/null +++ b/savedata/effs_export_base_fits.py @@ -0,0 +1,247 @@ +import inspect +import os +import platform +import re +import sys +import traceback +from optparse import AmbiguousOptionError, BadOptionError, OptionParser + +from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, StreamHandler, TimedRotatingFileHandler, WARNING, \ + __version__ as logbook_version + +sys.path.append(os.getcwd()) +import config + +from math import log + +try: + import wxversion +except ImportError: + wxversion = None + +try: + import sqlalchemy +except ImportError: + sqlalchemy = None + +pyfalog = Logger(__name__) + +class PassThroughOptionParser(OptionParser): + + def _process_args(self, largs, rargs, values): + while rargs: + try: + OptionParser._process_args(self, largs, rargs, values) + except (BadOptionError, AmbiguousOptionError) as e: + pyfalog.error("Bad startup option passed.") + largs.append(e.opt_str) + +usage = "usage: %prog [--root]" +parser = PassThroughOptionParser(usage=usage) +parser.add_option("-r", "--root", action="store_true", dest="rootsavedata", help="if you want pyfa to store its data in root folder, use this option", default=False) +parser.add_option("-w", "--wx28", action="store_true", dest="force28", help="Force usage of wxPython 2.8", default=False) +parser.add_option("-d", "--debug", action="store_true", dest="debug", help="Set logger to debug level.", default=False) +parser.add_option("-t", "--title", action="store", dest="title", help="Set Window Title", default=None) +parser.add_option("-s", "--savepath", action="store", dest="savepath", help="Set the folder for savedata", default=None) +parser.add_option("-l", "--logginglevel", action="store", dest="logginglevel", help="Set desired logging level [Critical|Error|Warning|Info|Debug]", default="Error") + +(options, args) = parser.parse_args() + +if options.rootsavedata is True: + config.saveInRoot = True + +config.debug = options.debug + +config.defPaths(options.savepath) + +try: + import requests + config.requestsVersion = requests.__version__ +except ImportError: + raise PreCheckException("Cannot import requests. You can download requests from https://pypi.python.org/pypi/requests.") + +import eos.db + +#if config.saVersion[0] > 0 or config.saVersion[1] >= 7: + # <0.7 doesn't have support for events ;_; (mac-deprecated) +config.sa_events = True +import eos.events + + # noinspection PyUnresolvedReferences +import service.prefetch # noqa: F401 + + # Make sure the saveddata db exists +if not os.path.exists(config.savePath): + os.mkdir(config.savePath) + +eos.db.saveddata_meta.create_all() + +import json +from service.fit import Fit +from effs_stat_export import parseNeededFitDetails + +from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, Table +from sqlalchemy.orm import relation, mapper, synonym, deferred +from eos.db import gamedata_session +from eos.db import gamedata_meta +from eos.db.gamedata.metaGroup import metatypes_table, items_table +from eos.db.gamedata.group import groups_table + +from eos.gamedata import AlphaClone, Attribute, Category, Group, Item, MarketGroup, \ + MetaGroup, AttributeInfo, MetaData, Effect, ItemEffect, Traits +from eos.db.gamedata.traits import traits_table +from eos.saveddata.mode import Mode + +def exportBaseShips(opts): + if opts: + if opts.outputpath: + basePath = opts.outputpath + elif opts.savepath: + basePath = opts.savepath + else: + basePath = config.savePath + os.sep + else: + basePath = config.savePath + os.sep + if basePath[len(basePath) - 1] != os.sep: + basePath = basePath + os.sep + outputBaseline = open(basePath + 'shipBaseJSON.js', 'w') + outputBaseline.write('let shipBaseJSON = JSON.stringify([') + shipCata = eos.db.getItemsByCategory('Ship') + baseLimit = 1000 + baseN = 0 + nameReqBase = ''; + for ship in iter(shipCata): + if baseN < baseLimit and nameReqBase in ship.name: + print(ship.name) + print(ship.groupID) + dna = str(ship.ID) + if ship.groupID == 963: + stats = t3cGetStatSet(dna, ship.name, ship.groupID, ship.raceID) + elif ship.groupID == 1305: + stats = t3dGetStatSet(dna, ship.name, ship.groupID, ship.raceID) + else: + stats = setFitFromString(dna, ship.name, ship.groupID) + outputBaseline.write(stats) + outputBaseline.write(',\n') + baseN += 1 + outputBaseline.write(']);\nexport {shipBaseJSON};') + outputBaseline.close() + +def t3dGetStatSet(dnaString, shipName, groupID, raceID): + t3dModeGroupFilter = Group.groupID == 1306 + data = list(gamedata_session.query(Group).options().filter(t3dModeGroupFilter).all()) + #Normally we would filter this via the raceID, + #Unfortunately somebody fat fingered the Jackdaw modes raceIDs as 4 (Amarr) not 1 (Caldari) + # t3dModes = list(filter(lambda mode: mode.raceID == raceID, data[0].items)) #Line for if/when they fix it + t3dModes = list(filter(lambda mode: shipName in mode.name, data[0].items)) + shipModeData = '' + n = 0 + while n < len(t3dModes): + dna = dnaString + ':' + str(t3dModes[n].ID) + ';1' + shipModeData += setFitFromString(dna, t3dModes[n].name, groupID) + ',\n' + n += 1 + return shipModeData + +def t3cGetStatSet(dnaString, shipName, groupID, raceID): + subsystemFilter = Group.categoryID == 32 + data = list(gamedata_session.query(Group).options().filter(subsystemFilter).all()) + # multi dimension array to hold the t3c subsystems as ss[index of subsystem type][index subsystem item] + ss = [[], [], [], []] + s = 0 + while s < 4: + ss[s] = list(filter(lambda subsystem: subsystem.raceID == raceID, data[s].items)) + s += 1 + print(shipName) + print(ss) + shipPermutationData = '' + n = 0 + a = 0 + while a < 3: + b = 0 + while b < 3: + c = 0 + while c < 3: + d = 0 + while d < 3: + dna = dnaString + ':' + str(ss[0][a].ID) \ + + ';1:' + str(ss[1][b].ID) + ';1:' + str(ss[2][c].ID) \ + + ';1:' + str(ss[3][d].ID) + ';1' + name = shipName + str(a) + str(b) + str(c) + str(d) + shipPermutationData += setFitFromString(dna, name, groupID) + ',\n' + d += 1 + n += 1 + c += 1 + b += 1 + a += 1 + print(str(n) + ' subsystem conbinations for ' + shipName) + return shipPermutationData +try: + armorLinkShip = eos.db.searchFits('armor links')[0] + infoLinkShip = eos.db.searchFits('information links')[0] + shieldLinkShip = eos.db.searchFits('shield links')[0] + skirmishLinkShip = eos.db.searchFits('skirmish links')[0] +except: + armorLinkShip = None + infoLinkShip = None + shieldLinkShip = None + skirmishLinkShip = None + +def setFitFromString(dnaString, fitName, groupID) : + if armorLinkShip == None: + print('Cannot find correct link fits for base calculations') + return '' + modArray = dnaString.split(':') + additionalModeFit = '' + fitL = Fit() + fitID = fitL.newFit(int(modArray[0]), fitName) + fit = eos.db.getFit(fitID) + ammoArray = [] + n = -1 + for mod in iter(modArray): + n = n + 1 + if n > 0: + modSp = mod.split(';') + if len(modSp) == 2: + k = 0 + while k < int(modSp[1]): + k = k + 1 + itemID = int(modSp[0]) + item = eos.db.getItem(int(modSp[0]), eager=("attributes", "group.category")) + cat = item.category.name + print(cat) + if cat == 'Drone': + fitL.addDrone(fitID, itemID, int(modSp[1]), recalc=False) + k += int(modSp[1]) + if cat == 'Fighter': + fitL.addFighter(fitID, itemID, recalc=False) + k += 100 + if fitL.isAmmo(int(modSp[0])): + k += 100 + ammoArray.append(int(modSp[0])); + # Set mode if module is a mode on a t3d + if item.groupID == 1306 and groupID == 1305: + fitL.setMode(fitID, Mode(item)) + else: + fitL.appendModule(fitID, int(modSp[0])) + fit = eos.db.getFit(fitID) + for ammo in iter(ammoArray): + fitL.setAmmo(fitID, ammo, list(filter(lambda mod: str(mod).find('name') > 0, fit.modules))) + if len(fit.drones) > 0: + fit.drones[0].amountActive = fit.drones[0].amount + eos.db.commit() + for fighter in iter(fit.fighters): + for ability in fighter.abilities: + if ability.effect.handlerName == u'fighterabilityattackm' and ability.active == True: + for abilityAltRef in fighter.abilities: + if abilityAltRef.effect.isImplemented: + abilityAltRef.active = True + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + print(list(filter(lambda mod: mod.item and mod.item.groupID in [1189, 658], fit.modules))) + fitL.addCommandFit(fit.ID, armorLinkShip) + fitL.addCommandFit(fit.ID, shieldLinkShip) + fitL.addCommandFit(fit.ID, skirmishLinkShip) + fitL.addCommandFit(fit.ID, infoLinkShip) + jsonStr = parseNeededFitDetails(fit, groupID) + Fit.deleteFit(fitID) + return jsonStr + additionalModeFit diff --git a/savedata/effs_export_pyfa_fits.py b/savedata/effs_export_pyfa_fits.py new file mode 100644 index 000000000..f49cc0971 --- /dev/null +++ b/savedata/effs_export_pyfa_fits.py @@ -0,0 +1,53 @@ +import inspect +import os +import platform +import re +import sys +import traceback + +sys.path.append(os.getcwd()) +import config +from pyfa import options + +if options.rootsavedata is True: + config.saveInRoot = True +config.debug = options.debug +config.defPaths(options.savepath) + +import eos.db +# Make sure the saveddata db exists +if not os.path.exists(config.savePath): + os.mkdir(config.savePath) + +from effs_stat_export import parseNeededFitDetails + +def exportPyfaFits(opts): + if opts: + if opts.outputpath: + basePath = opts.outputpath + elif opts.savepath: + basePath = opts.savepath + else: + basePath = config.savePath + os.sep + else: + basePath = config.savePath + os.sep + if basePath[len(basePath) - 1] != os.sep: + basePath = basePath + os.sep + output = open(basePath + 'shipJSON.js', 'w') + output.write('let shipJSON = JSON.stringify([') + #The current storage system isn't going to hold more than 2500 fits as local browser storage is limited + limit = 2500 + skipTill = 0 + nameReq = '' + n = 0 + fitList = eos.db.getFitList() + for fit in fitList: + if limit == None or n < limit: + n += 1 + name = fit.ship.name + ': ' + fit.name + if n >= skipTill and nameReq in name: + stats = parseNeededFitDetails(fit, 0) + output.write(stats) + output.write(',\n') + output.write(']);\nexport {shipJSON};') + output.close() diff --git a/savedata/effs_process_html_export.py b/savedata/effs_process_html_export.py new file mode 100644 index 000000000..5d89a4547 --- /dev/null +++ b/savedata/effs_process_html_export.py @@ -0,0 +1,60 @@ +from effs_export_base_fits import * + +def effsFitsFromHTMLExport(opts): + if opts: + if opts.outputpath: + basePath = opts.outputpath + elif opts.savepath: + basePath = opts.savepath + else: + basePath = config.savePath + os.sep + else: + basePath = config.savePath + os.sep + if basePath[len(basePath) - 1] != os.sep: + basePath = basePath + os.sep + output = open(basePath + 'shipJSON.js', 'w') + output.write('let shipJSON = JSON.stringify([') + try: + with open('pyfaFits.html'): + fileLocation = 'pyfaFits.html' + except: + try: + d = config.savePath + os.sep + 'pyfaFits.html' + print(d) + with open(d): + fileLocation = d + except: + fileLocation = None; + limit = 10000 + n = 0 + skipTill = 0 + nameReq = '' + minimalExport = True + if fileLocation != None: + with open(fileLocation) as f: + for fullLine in f: + if limit == None or n < limit: + if n <= 1 and '' in fullLine: + minimalExport = False + n += 1 + fullIndex = fullLine.find('data-dna="') + minimalIndex = fullLine.find('/dna/') + if fullIndex >= 0: + startInd = fullLine.find('data-dna="') + 10 + elif minimalIndex >= 0 and minimalExport: + startInd = fullLine.find('/dna/') + 5 + else: + startInd = -1 + print(startInd) + if startInd >= 0: + line = fullLine[startInd:len(fullLine)] + endInd = line.find('::') + dna = line[0:endInd] + name = line[line.find('>') + 1:line.find('<')] + if n >= skipTill and nameReq in name: + print('name: ' + name + ' DNA: ' + dna + fullLine) + stats = setFitFromString(dna, name, 0) + output.write(stats) + output.write(',\n') + output.write(']);\nexport {shipJSON};') + output.close() diff --git a/savedata/effs_util.py b/savedata/effs_util.py new file mode 100644 index 000000000..1b77238f8 --- /dev/null +++ b/savedata/effs_util.py @@ -0,0 +1,56 @@ +from optparse import AmbiguousOptionError, BadOptionError, OptionParser + +class PassThroughOptionParser(OptionParser): + + def _process_args(self, largs, rargs, values): + while rargs: + try: + OptionParser._process_args(self, largs, rargs, values) + except (BadOptionError, AmbiguousOptionError) as e: + pyfalog.error("Bad startup option passed.") + largs.append(e.opt_str) + +usage = "usage: %prog [options]" +parser = PassThroughOptionParser(usage=usage) +parser.add_option( + "-f", "--exportfits", action="store_true", dest="exportfits", \ + help="Export this copy of pyfa's local fits to a shipJSON file that Eve Fleet Simulator can import from", \ + default=False) +parser.add_option( + "-b", "--exportbaseships", action="store_true", dest="exportbaseships", \ + help="Export ship stats to a shipBaseJSON file used by Eve Fleet Simulator", \ + default=False) +parser.add_option( + "-c", "--convertfitsfromhtml", action="store_true", dest="convertfitsfromhtml", \ + help="Convert an exported pyfaFits.html file to a shipJSON file that Eve Fleet Simulator can import from\n" + + " Note this process loses data like fleet boosters as the DNA format exported by to html contains limited data", \ + default=False) +parser.add_option("-s", "--savepath", action="store", dest="savepath", help="Set the folder for savedata", default=None) +parser.add_option( + "-o", "--outputpath", action="store", dest="outputpath", + help="Output directory, defaults to the savepath", default=None) + + +(options, args) = parser.parse_args() + +if options.exportfits: + from effs_export_pyfa_fits import exportPyfaFits + exportPyfaFits(options) + +if options.exportbaseships: + from effs_export_base_fits import exportBaseShips + exportBaseShips(options) + +if options.convertfitsfromhtml: + from effs_process_html_export import effsFitsFromHTMLExport + effsFitsFromHTMLExport(options) + +#stuff bellow this point is purely scrap diagnostic stuff and should not be public (as it's scrawl) +def printGroupData(): + from eos.db import gamedata_session + from eos.gamedata import Group, Category + filterVal = Group.categoryID == 6 + data = gamedata_session.query(Group).options().list(filter(filterVal).all()) + for group in data: + print(group.groupName + ' groupID: ' + str(group.groupID)) + return '' From d61ab0ff5a5498ffdc3a930e11458eddef318e49 Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Sat, 16 Jun 2018 04:50:55 -0400 Subject: [PATCH 12/49] Refactoring for various EFS export code --- effs_stat_export.py | 592 +++++++++++------- eos/effects/shipdronescoutthermaldamagegf2.py | 2 +- savedata/effs_export_base_fits.py | 6 +- savedata/effs_export_pyfa_fits.py | 4 +- savedata/effs_util.py | 158 ++++- savedata/getmods.py | 88 +++ savedata/makeAndDiffCheck.sh | 55 ++ 7 files changed, 689 insertions(+), 216 deletions(-) create mode 100644 savedata/getmods.py create mode 100755 savedata/makeAndDiffCheck.sh diff --git a/effs_stat_export.py b/effs_stat_export.py index c89d0f781..e723f5485 100755 --- a/effs_stat_export.py +++ b/effs_stat_export.py @@ -8,126 +8,174 @@ from math import log import eos.db -eos.db.saveddata_meta.create_all() - import json from service.fit import Fit +from service.market import Market +from eos.enum import Enum +from eos.saveddata.module import Hardpoint, Slot, Module +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 + +eos.db.saveddata_meta.create_all() + + +class RigSize(Enum): + # Matches to item attribute 'rigSize' on ship and rig items + SMALL = 1 + MEDIUM = 2 + LARGE = 3 + CAPITAL = 4 + def attrDirectMap(values, target, source): for val in values: target[val] = source.itemModifiedAttributes[val] + def getT2MwdSpeed(fit, fitL): fitID = fit.ID propID = None + shipHasMedSlots = fit.ship.itemModifiedAttributes['medSlots'] > 0 + shipPower = fit.ship.itemModifiedAttributes['powerOutput'] + # Monitors have a 99% reduction to prop mod power requirements + if fit.ship.name == 'Monitor': + shipPower *= 100 rigSize = fit.ship.itemModifiedAttributes['rigSize'] - if rigSize == 1 and fit.ship.itemModifiedAttributes['medSlots'] > 0: - propID = 440 - elif rigSize == 2 and fit.ship.itemModifiedAttributes['medSlots'] > 0: - propID = 12076 - elif rigSize == 3 and fit.ship.itemModifiedAttributes['medSlots'] > 0: - propID = 12084 - elif rigSize == 4 and fit.ship.itemModifiedAttributes['medSlots'] > 0: - if fit.ship.itemModifiedAttributes['powerOutput'] > 60000: - propID = 41253 - else: - propID = 12084 - elif rigSize == None and fit.ship.itemModifiedAttributes['medSlots'] > 0: - propID = 440 - if propID: - fitL.appendModule(fitID, propID) - fitL.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 - fitL.removeModule(fitID, mwdPosition) - fitL.recalc(fit) - fit = eos.db.getFit(fitID) - return mwdPropSpeed + 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 + fitL.appendModule(fitID, propID) + fitL.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 + fitL.removeModule(fitID, mwdPosition) + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + return mwdPropSpeed + def getPropData(fit, fitL): fitID = fit.ID - propMods = list(filter(lambda mod: mod.item and mod.item.groupID in [46], fit.modules)) - possibleMWD = list(filter(lambda mod: 'signatureRadiusBonus' in mod.item.attributes, propMods)) - if len(possibleMWD) > 0 and possibleMWD[0].state > 0: - mwd = possibleMWD[0] - oldMwdState = mwd.state - mwd.state = 0 + propGroupId = getGroup('Propulsion Module').ID + propMods = filter(lambda mod: mod.item and mod.item.groupID == propGroupId, 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 = 0 fitL.recalc(fit) fit = eos.db.getFit(fitID) sp = fit.maxSpeed sig = fit.ship.itemModifiedAttributes['signatureRadius'] - mwd.state = oldMwdState + propWithBloom.state = oldPropState fitL.recalc(fit) fit = eos.db.getFit(fitID) return {'usingMWD': True, 'unpropedSpeed': sp, 'unpropedSig': sig} - return {'usingMWD': False, 'unpropedSpeed': fit.maxSpeed, 'unpropedSig': fit.ship.itemModifiedAttributes['signatureRadius']} + return { + 'usingMWD': False, + 'unpropedSpeed': fit.maxSpeed, + 'unpropedSig': fit.ship.itemModifiedAttributes['signatureRadius'] + } + def getOutgoingProjectionData(fit): # This is a subset of module groups capable of projection and a superset of those currently used by efs - projectedModGroupIds = [ - 41, 52, 65, 67, 68, 71, 80, 201, 208, 291, 325, 379, 585, - 842, 899, 1150, 1154, 1189, 1306, 1672, 1697, 1698, 1815, 1894 + 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.groupID in projectedModGroupIds, fit.modules)) + modGroupIds = list(map(lambda s: getGroup(s).ID, modGroupNames)) + modGroupData = dict(map(lambda name, gid: (name, {'name': name, 'id': gid}), + modGroupNames, modGroupIds)) + projectedMods = list(filter(lambda mod: mod.item and mod.item.groupID in modGroupIds, fit.modules)) projections = [] for mod in projectedMods: stats = {} - if mod.item.groupID == 65 or mod.item.groupID == 1672: + if mod.item.groupID in [modGroupData['Stasis Web']['id'], modGroupData['Stasis Grappler']['id']]: stats['type'] = 'Stasis Web' stats['optimal'] = mod.itemModifiedAttributes['maxRange'] attrDirectMap(['duration', 'speedFactor'], stats, mod) - elif mod.item.groupID == 291: + elif mod.item.groupID == modGroupData['Weapon Disruptor']['id']: stats['type'] = 'Weapon Disruptor' stats['optimal'] = mod.itemModifiedAttributes['maxRange'] stats['falloff'] = mod.itemModifiedAttributes['falloffEffectiveness'] attrDirectMap([ - 'trackingSpeedBonus', 'maxRangeBonus', 'falloffBonus', 'aoeCloudSizeBonus',\ - 'aoeVelocityBonus', 'missileVelocityBonus', 'explosionDelayBonus'\ + 'trackingSpeedBonus', 'maxRangeBonus', 'falloffBonus', 'aoeCloudSizeBonus', + 'aoeVelocityBonus', 'missileVelocityBonus', 'explosionDelayBonus' ], stats, mod) - elif mod.item.groupID == 68: + elif mod.item.groupID == modGroupData['Energy Nosferatu']['id']: stats['type'] = 'Energy Nosferatu' attrDirectMap(['powerTransferAmount', 'energyNeutralizerSignatureResolution'], stats, mod) - elif mod.item.groupID == 71: + elif mod.item.groupID == modGroupData['Energy Neutralizer']['id']: stats['type'] = 'Energy Neutralizer' attrDirectMap([ - 'energyNeutralizerSignatureResolution','entityCapacitorLevelModifierSmall',\ - 'entityCapacitorLevelModifierMedium', 'entityCapacitorLevelModifierLarge',\ - 'energyNeutralizerAmount'\ + 'energyNeutralizerSignatureResolution', 'entityCapacitorLevelModifierSmall', + 'entityCapacitorLevelModifierMedium', 'entityCapacitorLevelModifierLarge', + 'energyNeutralizerAmount' ], stats, mod) - elif mod.item.groupID == 41 or mod.item.groupID == 1697: + elif mod.item.groupID in [modGroupData['Remote Shield Booster']['id'], + modGroupData['Ancillary Remote Shield Booster']['id']]: stats['type'] = 'Remote Shield Booster' attrDirectMap(['shieldBonus'], stats, mod) - elif mod.item.groupID == 325 or mod.item.groupID == 1698: + elif mod.item.groupID in [modGroupData['Remote Armor Repairer']['id'], + modGroupData['Ancillary Remote Armor Repairer']['id']]: stats['type'] = 'Remote Armor Repairer' attrDirectMap(['armorDamageAmount'], stats, mod) - elif mod.item.groupID == 52: + elif mod.item.groupID == modGroupData['Warp Scrambler']['id']: stats['type'] = 'Warp Scrambler' attrDirectMap(['activationBlockedStrenght', 'warpScrambleStrength'], stats, mod) - elif mod.item.groupID == 379: + elif mod.item.groupID == modGroupData['Target Painter']['id']: stats['type'] = 'Target Painter' attrDirectMap(['signatureRadiusBonus'], stats, mod) - elif mod.item.groupID == 208: + elif mod.item.groupID == modGroupData['Sensor Dampener']['id']: stats['type'] = 'Sensor Dampener' attrDirectMap(['maxTargetRangeBonus', 'scanResolutionBonus'], stats, mod) - elif mod.item.groupID == 201: + elif mod.item.groupID == modGroupData['ECM']['id']: stats['type'] = 'ECM' attrDirectMap([ - 'scanGravimetricStrengthBonus', 'scanMagnetometricStrengthBonus',\ - 'scanRadarStrengthBonus', 'scanLadarStrengthBonus',\ + 'scanGravimetricStrengthBonus', 'scanMagnetometricStrengthBonus', + 'scanRadarStrengthBonus', 'scanLadarStrengthBonus', ], stats, mod) - elif mod.item.groupID == 80: + elif mod.item.groupID == modGroupData['Burst Jammer']['id']: stats['type'] = 'Burst Jammer' mod.itemModifiedAttributes['maxRange'] = mod.itemModifiedAttributes['ecmBurstRange'] attrDirectMap([ - 'scanGravimetricStrengthBonus', 'scanMagnetometricStrengthBonus',\ - 'scanRadarStrengthBonus', 'scanLadarStrengthBonus',\ + 'scanGravimetricStrengthBonus', 'scanMagnetometricStrengthBonus', + 'scanRadarStrengthBonus', 'scanLadarStrengthBonus', ], stats, mod) - elif mod.item.groupID == 1189: + elif mod.item.groupID == modGroupData['Micro Jump Drive']['id']: stats['type'] = 'Micro Jump Drive' mod.itemModifiedAttributes['maxRange'] = 0 attrDirectMap(['moduleReactivationDelay'], stats, mod) - if mod.itemModifiedAttributes['maxRange'] == None: + if mod.itemModifiedAttributes['maxRange'] is None: print(mod.item.name) print(mod.itemModifiedAttributes.items()) raise ValueError('Projected module lacks a maxRange') @@ -137,13 +185,14 @@ def getOutgoingProjectionData(fit): projections.append(stats) return projections + def getModuleNames(fit): moduleNames = [] highSlotNames = [] midSlotNames = [] lowSlotNames = [] rigSlotNames = [] - miscSlotNames = [] #subsystems ect + miscSlotNames = [] # subsystems ect for mod in fit.modules: if mod.slot == 3: modSlotNames = highSlotNames @@ -156,8 +205,8 @@ def getModuleNames(fit): elif mod.slot == 5: modSlotNames = miscSlotNames try: - if mod.item != None: - if mod.charge != None: + if mod.item is not None: + if mod.charge is not None: modSlotNames.append(mod.item.name + ': ' + mod.charge.name) else: modSlotNames.append(mod.item.name) @@ -167,8 +216,12 @@ def getModuleNames(fit): print(vars(mod)) print('could not find name for module') print(fit.modules) - for modInfo in [['High Slots:'], highSlotNames, ['', 'Med Slots:'], midSlotNames, ['', 'Low Slots:'], lowSlotNames, ['', 'Rig Slots:'], rigSlotNames]: + for modInfo in [ + ['High Slots:'], highSlotNames, ['', 'Med Slots:'], midSlotNames, + ['', 'Low Slots:'], lowSlotNames, ['', 'Rig Slots:'], rigSlotNames + ]: moduleNames.extend(modInfo) + if len(miscSlotNames) > 0: moduleNames.append('') moduleNames.append('Subsystems:') @@ -201,19 +254,37 @@ def getModuleNames(fit): moduleNames.append(commandFit.name) return moduleNames + +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'], + } + + 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: - stats = groups[wepGroup][0] - c = groups[wepGroup][1] + for wepGroup in groups.values(): + stats = wepGroup[0] + n = wepGroup[1] tracking = 0 maxVelocity = 0 explosionDelay = 0 @@ -221,11 +292,12 @@ def getWeaponSystemData(fit): explosionRadius = 0 explosionVelocity = 0 aoeFieldRange = 0 - if stats.hardpoint == 2: + if stats.hardpoint == Hardpoint.TURRET: tracking = stats.itemModifiedAttributes['trackingSpeed'] typeing = 'Turret' name = stats.item.name + ', ' + stats.charge.name - elif stats.hardpoint == 1 or 'Bomb Launcher' in stats.item.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.chargeModifiedAttributes['maxVelocity'] explosionDelay = stats.chargeModifiedAttributes['explosionDelay'] damageReductionFactor = stats.chargeModifiedAttributes['aoeDamageReductionFactor'] @@ -233,140 +305,240 @@ def getWeaponSystemData(fit): explosionVelocity = stats.chargeModifiedAttributes['aoeVelocity'] typeing = 'Missile' name = stats.item.name + ', ' + stats.charge.name - elif stats.hardpoint == 0: + elif stats.hardpoint == Hardpoint.NONE: aoeFieldRange = stats.itemModifiedAttributes['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 * c, 'capUse': stats.capUse * c, '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 * c, 'tracking': tracking,\ - 'maxVelocity': maxVelocity, 'explosionDelay': explosionDelay, 'damageReductionFactor': damageReductionFactor,\ - 'explosionRadius': explosionRadius, 'explosionVelocity': explosionVelocity, 'aoeFieldRange': aoeFieldRange\ + 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 } weaponSystems.append(statDict) - #if fit.droneDPS > 0: for drone in fit.drones: if drone.dps[0] > 0 and drone.amountActive > 0: - newTracking = drone.itemModifiedAttributes['trackingSpeed'] / (drone.itemModifiedAttributes['optimalSigRadius'] / 40000) - statDict = {'dps': drone.dps[0], 'cycleTime': drone.cycleTime, 'type': 'Drone',\ - 'optimal': drone.maxRange, 'name': drone.item.name, 'falloff': drone.falloff,\ - 'maxSpeed': drone.itemModifiedAttributes['maxVelocity'], 'tracking': newTracking,\ - 'volley': drone.dps[1]\ + droneAttr = drone.itemModifiedAttributes + # 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.itemModifiedAttributes abilities = [] - #for ability in fighter.abilities: - if 'fighterAbilityAttackMissileDamageEM' in fighter.itemModifiedAttributes: + if 'fighterAbilityAttackMissileDamageEM' in fighterAttr: baseRef = 'fighterAbilityAttackMissile' - baseRefDam = baseRef + 'Damage' - damageReductionFactor = log(fighter.itemModifiedAttributes[baseRef + 'ReductionFactor']) / log(fighter.itemModifiedAttributes[baseRef + 'ReductionSensitivity']) - abBaseDamage = fighter.itemModifiedAttributes[baseRefDam + 'EM'] + fighter.itemModifiedAttributes[baseRefDam + 'Therm'] + fighter.itemModifiedAttributes[baseRefDam + 'Exp'] + fighter.itemModifiedAttributes[baseRefDam + 'Kin'] - abDamage = abBaseDamage * fighter.itemModifiedAttributes[baseRefDam + 'Multiplier'] - ability = {'name': 'RegularAttack', 'volley': abDamage * fighter.amountActive, 'explosionRadius': fighter.itemModifiedAttributes[baseRef + 'ExplosionRadius'],\ - 'explosionVelocity': fighter.itemModifiedAttributes[baseRef + 'ExplosionVelocity'], 'optimal': fighter.itemModifiedAttributes[baseRef + 'RangeOptimal'],\ - 'damageReductionFactor': damageReductionFactor, 'rof': fighter.itemModifiedAttributes[baseRef + 'Duration'],\ - } + ability = getFighterAbilityData(fighterAttr, fighter, baseRef) abilities.append(ability) - if 'fighterAbilityMissilesDamageEM' in fighter.itemModifiedAttributes: + if 'fighterAbilityMissilesDamageEM' in fighterAttr: baseRef = 'fighterAbilityMissiles' - baseRefDam = baseRef + 'Damage' - damageReductionFactor = log(fighter.itemModifiedAttributes[baseRefDam + 'ReductionFactor']) / log(fighter.itemModifiedAttributes[baseRefDam + 'ReductionSensitivity']) - abBaseDamage = fighter.itemModifiedAttributes[baseRefDam + 'EM'] + fighter.itemModifiedAttributes[baseRefDam + 'Therm'] + fighter.itemModifiedAttributes[baseRefDam + 'Exp'] + fighter.itemModifiedAttributes[baseRefDam + 'Kin'] - abDamage = abBaseDamage * fighter.itemModifiedAttributes[baseRefDam + 'Multiplier'] - ability = {'name': 'MissileAttack', 'volley': abDamage * fighter.amountActive, 'explosionRadius': fighter.itemModifiedAttributes[baseRef + 'ExplosionRadius'],\ - 'explosionVelocity': fighter.itemModifiedAttributes[baseRef + 'ExplosionVelocity'], 'optimal': fighter.itemModifiedAttributes[baseRef + 'Range'],\ - 'damageReductionFactor': damageReductionFactor, 'rof': fighter.itemModifiedAttributes[baseRef + 'Duration'],\ - } + ability = getFighterAbilityData(fighterAttr, fighter, baseRef) abilities.append(ability) - statDict = {'dps': fighter.dps[0], 'type': 'Fighter', 'name': fighter.item.name,\ - 'maxSpeed': fighter.itemModifiedAttributes['maxVelocity'], 'abilities': abilities, 'ehp': fighter.itemModifiedAttributes['shieldCapacity'] / 0.8875 * fighter.amountActive,\ - 'volley': fighter.dps[1], 'signatureRadius': fighter.itemModifiedAttributes['signatureRadius']\ + 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 -def getWeaponBonusMultipliers(fit): - multipliers = {'turret': 1, 'launcher': 1, 'droneBandwidth': 1} - from eos.db import gamedata_session - from eos.gamedata import Traits - filterVal = Traits.typeID == fit.shipID - data = gamedata_session.query(Traits).options().filter(filterVal).all() - roleBonusMode = False - if len(data) == 0: - return multipliers - previousTypedBonus = 0 - previousDroneTypeBonus = 0 - for bonusText in data[0].traitText.splitlines(): - bonusText = bonusText.lower() - if 'per skill level' in bonusText: - roleBonusMode = False - if 'role bonus' in bonusText or 'misc bonus' in bonusText: - roleBonusMode = True - multi = 1 - if 'damage' in bonusText and not any(e in bonusText for e in ['control', 'heat']): - splitText = bonusText.split('%') - if (float(splitText[0]) > 0) == False: - print('damage bonus split did not parse correctly!') - print(float(splitText[0])) - if roleBonusMode: - addedMulti = float(splitText[0]) - else: - addedMulti = float(splitText[0]) * 5 - if any(e in bonusText for e in [' em', 'thermal', 'kinetic', 'explosive']): - if addedMulti > previousTypedBonus: - previousTypedBonus = addedMulti - else: - addedMulti = 0 - if any(e in bonusText for e in ['heavy drone', 'medium drone', 'light drone', 'sentry drone']): - if addedMulti > previousDroneTypeBonus: - previousDroneTypeBonus = addedMulti - else: - addedMulti = 0 - multi = 1 + (addedMulti / 100) - elif 'rate of fire' in bonusText: - splitText = bonusText.split('%') - if (float(splitText[0]) > 0) == False: - print('rate of fire bonus split did not parse correctly!') - print(float(splitText[0])) - if roleBonusMode: - rofMulti = float(splitText[0]) - else: - rofMulti = float(splitText[0]) * 5 - multi = 1 / (1 - (rofMulti / 100)) - if multi > 1: - if 'drone' in bonusText.lower(): - multipliers['droneBandwidth'] *= multi - elif 'turret' in bonusText.lower(): - multipliers['turret'] *= multi - elif any(e in bonusText for e in ['missile', 'torpedo']): - multipliers['launcher'] *= multi - return multipliers -def getShipSize(groupID): - # Sizings are somewhat arbitrary but allow for a more managable number of top level groupings in a tree structure. - shipSizes = ['Frigate', 'Destroyer', 'Cruiser', 'Battlecruiser', 'Battleship', 'Capital', 'Industrial', 'Misc'] - if groupID in [25, 31, 237, 324, 830, 831, 834, 893, 1283, 1527]: - return shipSizes[0] - elif groupID in [420, 541, 1305, 1534]: - return shipSizes[1] - elif groupID in [26, 358, 832, 833, 894, 906, 963]: - return shipSizes[2] - elif groupID in [419, 540, 1201]: - return shipSizes[3] - elif groupID in [27, 381, 898, 900]: - return shipSizes[4] - elif groupID in [30, 485, 513, 547, 659, 883, 902, 1538]: - return shipSizes[5] - elif groupID in [28, 380, 1202, 463, 543, 941]: - return shipSizes[6] - elif groupID in [29, 1022]: - return shipSizes[7] + +wepTestSet = {} + + +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 wepTestSet.keys(): + return wepTestSet[setType] else: - sizeNotFoundMsg = 'ShipSize not found for groupID: ' + str(groupID) - print(sizeNotFoundMsg) - return sizeNotFoundMsg + wepTestSet[setType] = [] + modSet = 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) + + mkt = 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.itemModifiedAttributes['chargeGroup1']).all()) + mods = [mod] + charges = [clist[0]] + if setType == 'launcher': + # We don't want variations of missiles we already have + prevCharges = list(mkt.getVariationsByItems(charges)) + testCharges = [] + for charge in clist: + if charge not in prevCharges: + testCharges.append(charge) + prevCharges += mkt.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 + + +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.itemModifiedAttributes) * d.itemModifiedAttributes['damageMultiplier'] + fitMultipliers['drones'] = list(map(getDroneMulti, tf.drones)) + + getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == Hardpoint.TURRET, f.modules) + getTurretMulti = lambda mod: mod.itemModifiedAttributes['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.chargeModifiedAttributes) / mod.cycleTime + fitMultipliers['launchers'] = list(map(getLauncherMulti, getFitLaunchers(tf))) + return fitMultipliers + + multipliers = {'turret': 1, 'launcher': 1, 'droneBandwidth': 1} + drones = getTestSet('drone') + launchers = getTestSet('launcher') + turrets = getTestSet('turret') + for weaponTypeSet in [turrets, launchers, drones]: + for mod in weaponTypeSet: + mod.owner = fit + turrets = list(filter(lambda mod: mod.itemModifiedAttributes['damageMultiplier'], turrets)) + launchers = list(filter(lambda mod: sumDamage(mod.chargeModifiedAttributes), launchers)) + # Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits. + tf = Fit.getInstance() + 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) + tf.recalc(fit) + return multipliers + + +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) + print(sizeNotFoundMsg) + return sizeNotFoundMsg + def parseNeededFitDetails(fit, groupID): includeShipTypeData = groupID > 0 @@ -377,13 +549,11 @@ def parseNeededFitDetails(fit, groupID): fitName = fit.name print('') print('name: ' + fit.name) - fitL = Fit() + fitL = Fit.getInstance() fitL.recalc(fit) fit = eos.db.getFit(fitID) fitModAttr = fit.ship.itemModifiedAttributes propData = getPropData(fit, fitL) - print(fitModAttr['rigSize']) - print(propData) mwdPropSpeed = fit.maxSpeed if includeShipTypeData: mwdPropSpeed = getT2MwdSpeed(fit, fitL) @@ -395,52 +565,52 @@ def parseNeededFitDetails(fit, groupID): launcherSlots = fitModAttr['launcherSlotsLeft'] if fitModAttr['launcherSlotsLeft'] is not None else 0 droneBandwidth = fitModAttr['droneBandwidth'] if fitModAttr['droneBandwidth'] is not None else 0 weaponBonusMultipliers = getWeaponBonusMultipliers(fit) - effectiveTurretSlots = round(turretSlots * weaponBonusMultipliers['turret'], 2); - effectiveLauncherSlots = round(launcherSlots * weaponBonusMultipliers['launcher'], 2); - effectiveDroneBandwidth = round(droneBandwidth * weaponBonusMultipliers['droneBandwidth'], 2); + 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 groupID == 485: + if groupID == getGroup('Dreadnought').ID: effectiveTurretSlots *= 9.4 effectiveLauncherSlots *= 15 hullResonance = { - 'exp': fitModAttr['explosiveDamageResonance'], 'kin': fitModAttr['kineticDamageResonance'], \ + 'exp': fitModAttr['explosiveDamageResonance'], 'kin': fitModAttr['kineticDamageResonance'], 'therm': fitModAttr['thermalDamageResonance'], 'em': fitModAttr['emDamageResonance'] } armorResonance = { - 'exp': fitModAttr['armorExplosiveDamageResonance'], 'kin': fitModAttr['armorKineticDamageResonance'], \ + 'exp': fitModAttr['armorExplosiveDamageResonance'], 'kin': fitModAttr['armorKineticDamageResonance'], 'therm': fitModAttr['armorThermalDamageResonance'], 'em': fitModAttr['armorEmDamageResonance'] } shieldResonance = { - 'exp': fitModAttr['shieldExplosiveDamageResonance'], 'kin': fitModAttr['shieldKineticDamageResonance'], \ + 'exp': fitModAttr['shieldExplosiveDamageResonance'], 'kin': fitModAttr['shieldKineticDamageResonance'], 'therm': fitModAttr['shieldThermalDamageResonance'], 'em': fitModAttr['shieldEmDamageResonance'] } resonance = {'hull': hullResonance, 'armor': armorResonance, 'shield': shieldResonance} shipSize = getShipSize(groupID) try: - parsable = { - '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'],\ - 'projectedModules': fit.projectedModules, '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'], 'rigSize': fitModAttr['rigSize'],\ - 'effectiveTurrets': effectiveTurretSlots, 'effectiveLaunchers': effectiveLauncherSlots,\ - 'effectiveDroneBandwidth': effectiveDroneBandwidth,\ - 'resonance': resonance, 'typeID': fit.shipID, 'groupID': groupID, 'shipSize': shipSize,\ - 'droneControlRange': fitModAttr['droneControlRange'], 'mass': fitModAttr['mass'],\ - 'moduleNames': moduleNames, 'projections': projections,\ - 'unpropedSpeed': propData['unpropedSpeed'], 'unpropedSig': propData['unpropedSig'],\ + 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'], + 'projectedModules': fit.projectedModules, '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'], 'rigSize': fitModAttr['rigSize'], + 'effectiveTurrets': effectiveTurretSlots, 'effectiveLaunchers': effectiveLauncherSlots, + 'effectiveDroneBandwidth': effectiveDroneBandwidth, + 'resonance': resonance, 'typeID': fit.shipID, 'groupID': groupID, 'shipSize': shipSize, + 'droneControlRange': fitModAttr['droneControlRange'], 'mass': fitModAttr['mass'], + 'moduleNames': moduleNames, 'projections': projections, + 'unpropedSpeed': propData['unpropedSpeed'], 'unpropedSig': propData['unpropedSig'], 'usingMWD': propData['usingMWD'], 'mwdPropSpeed': mwdPropSpeed } except TypeError: print('Error parsing fit:' + str(fit)) print(TypeError) parsable = {'name': fitName + 'Fit could not be correctly parsed'} - stringified = json.dumps(parsable, skipkeys=True) - return stringified + export = json.dumps(dataDict, skipkeys=True) + return export diff --git a/eos/effects/shipdronescoutthermaldamagegf2.py b/eos/effects/shipdronescoutthermaldamagegf2.py index afe18cb9d..54f7716e0 100644 --- a/eos/effects/shipdronescoutthermaldamagegf2.py +++ b/eos/effects/shipdronescoutthermaldamagegf2.py @@ -6,5 +6,5 @@ type = "passive" def handler(fit, ship, context): - fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drone Avionics"), + fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Light Drone Operation"), "thermalDamage", ship.getModifiedItemAttr("shipBonusGF2"), skill="Gallente Frigate") diff --git a/savedata/effs_export_base_fits.py b/savedata/effs_export_base_fits.py index e1be23e3b..c08c6c003 100644 --- a/savedata/effs_export_base_fits.py +++ b/savedata/effs_export_base_fits.py @@ -93,7 +93,10 @@ from eos.db.gamedata.traits import traits_table from eos.saveddata.mode import Mode def exportBaseShips(opts): + nameReq = '' if opts: + if opts.search: + nameReq = opts.search if opts.outputpath: basePath = opts.outputpath elif opts.savepath: @@ -109,9 +112,8 @@ def exportBaseShips(opts): shipCata = eos.db.getItemsByCategory('Ship') baseLimit = 1000 baseN = 0 - nameReqBase = ''; for ship in iter(shipCata): - if baseN < baseLimit and nameReqBase in ship.name: + if baseN < baseLimit and nameReq in ship.name: print(ship.name) print(ship.groupID) dna = str(ship.ID) diff --git a/savedata/effs_export_pyfa_fits.py b/savedata/effs_export_pyfa_fits.py index f49cc0971..82949f1c4 100644 --- a/savedata/effs_export_pyfa_fits.py +++ b/savedata/effs_export_pyfa_fits.py @@ -22,7 +22,10 @@ if not os.path.exists(config.savePath): from effs_stat_export import parseNeededFitDetails def exportPyfaFits(opts): + nameReq = '' if opts: + if opts.search: + nameReq = opts.search if opts.outputpath: basePath = opts.outputpath elif opts.savepath: @@ -38,7 +41,6 @@ def exportPyfaFits(opts): #The current storage system isn't going to hold more than 2500 fits as local browser storage is limited limit = 2500 skipTill = 0 - nameReq = '' n = 0 fitList = eos.db.getFitList() for fit in fitList: diff --git a/savedata/effs_util.py b/savedata/effs_util.py index 1b77238f8..93276d281 100644 --- a/savedata/effs_util.py +++ b/savedata/effs_util.py @@ -25,10 +25,14 @@ parser.add_option( help="Convert an exported pyfaFits.html file to a shipJSON file that Eve Fleet Simulator can import from\n" + " Note this process loses data like fleet boosters as the DNA format exported by to html contains limited data", \ default=False) -parser.add_option("-s", "--savepath", action="store", dest="savepath", help="Set the folder for savedata", default=None) +parser.add_option("-s", "--savepath", action="store", dest="savepath", + help="Set the folder for savedata", default=None) parser.add_option( "-o", "--outputpath", action="store", dest="outputpath", help="Output directory, defaults to the savepath", default=None) +parser.add_option( + '-i', "--search", action="store", dest="search", + help="Ignore ships and fits that don't contain the searched string", default=None) (options, args) = parser.parse_args() @@ -54,3 +58,155 @@ def printGroupData(): for group in data: print(group.groupName + ' groupID: ' + str(group.groupID)) return '' + +def printSizeData(): + from eos.db import gamedata_session + from eos.gamedata import Group + filterVal = Group.categoryID == 6 + data = gamedata_session.query(Group).options().filter(filterVal).all() + ships = gamedata_session.query(Group).options().filter(filterVal) + print(data) + print(vars(data[0])) + + shipSizes = ['Frigate', 'Destroyer', 'Cruiser', 'Battlecruiser', 'Battleship', 'Capital', 'Industrial', 'Misc'] + groupSets = [ + [25, 31, 237, 324, 830, 831, 834, 893, 1283, 1527], + [420, 541, 1305, 1534], + [26, 358, 832, 833, 894, 906, 963], + [419, 540, 1201], + [27, 381, 898, 900], + [30, 485, 513, 547, 659, 883, 902, 1538], + [28, 380, 1202, 463, 543, 941], + [29, 1022] + ] + i = 0 + while i < 8: + groupNames = '\'' + shipSizes[i] + '\': {\'name\': \'' + shipSizes[i] + '\', \'groupIDs\': groupIDFromGroupName([' + for gid in groupSets[i]: + if gid is not groupSets[i][0]: + groupNames = groupNames + '\', ' + groupNames = groupNames + '\'' + list(filter(lambda gr: gr.groupID == gid, data))[0].groupName + print(groupNames + '\'], data)}') + i = i + 1 + projectedModGroupIds = [ + 41, 52, 65, 67, 68, 71, 80, 201, 208, 291, 325, 379, 585, + 842, 899, 1150, 1154, 1189, 1306, 1672, 1697, 1698, 1815, 1894 + ] + from eos.db import gamedata_session + from eos.gamedata import Group + data = gamedata_session.query(Group).all() + groupNames = '' + for gid in projectedModGroupIds: + if gid is not projectedModGroupIds[0]: + groupNames = groupNames + '\', ' + print(gid) + groupNames = groupNames + '\'' + list(filter(lambda gr: gr.groupID == gid, data))[0].groupName + print(groupNames + '\'') + +def wepMultisFromTraitText(fit): + filterVal = Traits.typeID == fit.shipID + data = gamedata_session.query(Traits).options().filter(filterVal).all() + roleBonusMode = False + if len(data) == 0: + return multipliers + d = data[0] + s1 = str(vars(d)) + ds = s1.encode(encoding="utf-8", errors="ignore") + #print(ds) + previousTypedBonus = 0 + previousDroneTypeBonus = 0 + for bonusText in data[0].traitText.splitlines(): + bonusText = bonusText.lower() + if 'per skill level' in bonusText: + roleBonusMode = False + if 'role bonus' in bonusText or 'misc bonus' in bonusText: + roleBonusMode = True + multi = 1 + if 'damage' in bonusText and not any(e in bonusText for e in ['control', 'heat']): + splitText = bonusText.split('%') + if (float(splitText[0]) > 0) is False: + print('damage bonus split did not parse correctly!') + print(float(splitText[0])) + if roleBonusMode: + addedMulti = float(splitText[0]) + else: + addedMulti = float(splitText[0]) * 5 + if any(e in bonusText for e in [' em', 'thermal', 'kinetic', 'explosive']): + if addedMulti > previousTypedBonus: + previousTypedBonus = addedMulti + else: + addedMulti = 0 + if any(e in bonusText for e in ['heavy drone', 'medium drone', 'light drone', 'sentry drone']): + if addedMulti > previousDroneTypeBonus: + previousDroneTypeBonus = addedMulti + else: + addedMulti = 0 + multi = 1 + (addedMulti / 100) + elif 'rate of fire' in bonusText: + splitText = bonusText.split('%') + if (float(splitText[0]) > 0) is False: + print('rate of fire bonus split did not parse correctly!') + print(float(splitText[0])) + if roleBonusMode: + rofMulti = float(splitText[0]) + else: + rofMulti = float(splitText[0]) * 5 + multi = 1 / (1 - (rofMulti / 100)) + if multi > 1: + if 'drone' in bonusText.lower(): + multipliers['droneBandwidth'] *= multi + elif 'turret' in bonusText.lower(): + multipliers['turret'] *= multi + elif any(e in bonusText for e in ['missile', 'torpedo']): + multipliers['launcher'] *= multi + + +def examDiff(ai, bi, attr=False): + print('') + print('A:' + str(ai)) + print('B:' + str(bi)) + a = dict(map(lambda k: (k, getattr(ai, k)), dir(ai))) + b = dict(map(lambda k: (k, getattr(bi, k)), dir(bi))) + try: + print(a.keys()) + print('A:' + str(a)) + print(b.keys()) + print('B:' + str(b)) + print('A exclusive keys:') + for key in filter(lambda k: k not in b.keys(), a.keys()): + print(key) + print('B exclusive keys:') + for key in filter(lambda k: k not in a.keys(), b.keys()): + print(key) + print('A key/value pairs where B is None:') + for key in filter(lambda k: k in b.keys() and b[k] == None and a[k] != None, a.keys()): + print(key) + print(a[key]) + print('B key/value pairs where A is None:') + for key in filter(lambda k: k in a.keys() and a[k] == None and b[k] != None, b.keys()): + print(key) + print(b[key]) + except Exception as e: + if attr == True: + print('Could not print itemModifiedAttributes for a or b') + print(e) + else: + print('Checking itemModifiedAttributes diff') + examDiff(ai.itemModifiedAttributes, bi.itemModifiedAttributes, True) + if attr == False: + print('Checking itemModifiedAttributes diff') + examDiff(ai.itemModifiedAttributes, bi.itemModifiedAttributes, True) + print('') + +def groupIDFromGroupName(names, data=None): + # Group data can optionally be passed to the function to improve preformace with repeated calls. + if data is None: + data = gamedata_session.query(Group).all() + returnSingle = False + if not isinstance(names, list): + names = [names] + returnSingle = True + gidList = list(map(lambda incGroup: incGroup.groupID, (filter(lambda group: group.groupName in names, data)))) + if returnSingle: + return gidList[0] + return gidList diff --git a/savedata/getmods.py b/savedata/getmods.py new file mode 100644 index 000000000..16ef06103 --- /dev/null +++ b/savedata/getmods.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python +if __name__ == "__main__": + import argparse + import json + import os.path + import sys + sys.path.append(os.getcwd()) + + from eos import * + from eos.data.data_handler import JsonDataHandler + from eos.const.eos import * + + json_path = r"/path/to/Phobos/dump" + + parser = argparse.ArgumentParser(description="Figure out what actually effect does") + parser.add_argument("-e", "--effect", type=str, required=True, help="effect name") + args = parser.parse_args() + + # open a few files to get human-readable names for data (EOS strictly works with numerical identifiers) + with open(os.path.join(json_path, "dgmattribs.json"), mode='r', encoding="utf8") as file: + dgmattribs = json.load(file) + + with open(os.path.join(json_path, 'dgmeffects.json'), mode='r', encoding="utf8") as file: + dgmeffects = json.load(file) + + with open(os.path.join(json_path, 'invtypes.json'), mode='r', encoding="utf8") as file: + invtypes = json.load(file) + + with open(os.path.join(json_path, 'invgroups.json'), mode='r', encoding="utf8") as file: + invgroups = json.load(file) + + attr_id_name = {} + attr_id_penalized = {} + for row in dgmattribs: + attr_id_name[row['attributeID']] = row['attributeName'] + attr_id_penalized[row['attributeID']] = 'not penalized' if row['stackable'] else 'penalized' + + effect_id_name = {} + for row in dgmeffects: + effect_id_name[row['effectID']] = row['effectName'] + if row['effectName'] == args.effect: + effect_id = row['effectID'] + break + + type_id_name = {} + for _, row in invtypes.items(): + name = row.get("typeName_en-us", None) + if name: + type_id_name[row['typeID']] = name + + group_id_name = {} + for _, row in invgroups.items(): + group_id_name[row['groupID']] = row['groupName_en-us'] + + data_handler = JsonDataHandler(json_path) # Folder with Phobos data dump + cache_handler = JsonCacheHandler(os.path.join(json_path, "cache", "eos_tq.json.bz2")) + SourceManager.add('evedata', data_handler, cache_handler, make_default=True) + + effect = cache_handler.get_effect(effect_id) + modifiers = effect.modifiers + mod_counter = 1 + indent = ' ' + print('effect {}.py (id: {}) - build status is {}'.format(args.effect.lower(), effect_id, EffectBuildStatus(effect.build_status).name)) + for modifier in modifiers: + print('{}Modifier {}:'.format(indent, mod_counter)) + print('{0}{0}state: {1}'.format(indent, State(modifier.state).name)) + print('{0}{0}scope: {1}'.format(indent, Scope(modifier.scope).name)) + print('{0}{0}srcattr: {1} {2}'.format(indent, attr_id_name[modifier.src_attr], modifier.src_attr)) + print('{0}{0}operator: {1} {2}'.format(indent, Operator(modifier.operator).name, modifier.operator)) + print('{0}{0}tgtattr: {1} ({2}) {3}'.format( + indent, + attr_id_name[modifier.tgt_attr], + attr_id_penalized[modifier.tgt_attr],modifier.tgt_attr) + ) + print('{0}{0}location: {1}'.format(indent, Domain(modifier.domain).name)) + try: + filter_type = FilterType(modifier.filter_type).name + except ValueError: + filter_type = None + print('{0}{0}filter type: {1}'.format(indent, filter_type)) + if modifier.filter_type is None or modifier.filter_type in (FilterType.all_, FilterType.skill_self): + pass + elif modifier.filter_type == FilterType.skill: + print('{0}{0}filter value: {1}'.format(indent, type_id_name[modifier.filter_value])) + elif modifier.filter_type == FilterType.group: + print('{0}{0}filter value: {1}'.format(indent, group_id_name[modifier.filter_value])) + mod_counter += 1 diff --git a/savedata/makeAndDiffCheck.sh b/savedata/makeAndDiffCheck.sh new file mode 100755 index 000000000..6b3bb7640 --- /dev/null +++ b/savedata/makeAndDiffCheck.sh @@ -0,0 +1,55 @@ +#!/bin/bash +if [[ $2 == -v ]] ; then + MUTE=False +else + MUTE=TRUE +fi +EXPECTERRORS=False +if [[ $3 == --search ]] ; then + if [[ $5 == --expect-errors ]] ; then + EXPECTERRORS=True + fi +else + if [[ $3 == --expect-errors ]] ; then + EXPECTERRORS=True + fi +fi +if [[ $1 == -f ]] ; then + if [[ $MUTE == TRUE ]] ; then + python3opt savedata/effs_util.py\ -f | grep awgahwogfa + else + python3opt savedata/effs_util.py\ -f\ --search=$4 + fi +elif [[ $1 == -b ]] ; then + if [[ $MUTE == TRUE ]] ; then + python3opt savedata/effs_util.py\ -b | grep awgahwogfa + else + python3opt savedata/effs_util.py\ -b\ --search=$4 + fi +elif [[ $1 == -u ]] ; then + if [[ $MUTE == TRUE ]] ; then + python3opt savedata/effs_util.py\ -b\ -f\ -o\ .. | grep awgahwogfa + else + python3opt savedata/effs_util.py\ -b\ -f\ -o\ .. + fi +elif [[ $1 == -a ]] ; then + if [[ $MUTE == TRUE ]] ; then + python3opt savedata/effs_util.py\ -b\ -f | grep awgahwogfa + else + python3opt savedata/effs_util.py\ -b\ -f\ --search=$4 + fi +else + echo Defaulting to fits and base ships.\n + if [[ $MUTE == TRUE ]] ; then + python3opt savedata/effs_util.py\ -b\ -f | grep awgahwogfa + else + python3opt savedata/effs_util.py\ -b\ -f\ --search=$4 + fi +fi +if [[ $EXPECTERRORS == True ]] ; then + echo Expecting non standard output, this should only be used for testing +else +diff -s --color=always ../shipJSON.js ~/.pyfa/shipJSON.js | grep -m 3 --color '' +diff -s --color=always ../shipBaseJSON.js ~/.pyfa/shipBaseJSON.js | grep -m 3 --color '' +/home/stock/scripts/Pyfa/.tox/pep8/bin/flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py,floatspin.py --ignore=E121,E126,E127,E128,E203,E731,F401,E722,E741 effs_stat_export.py --max-line-length=165 +fi From 7af5c17015127935f58e9bd8336b1082bb28360d Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Fri, 6 Jul 2018 14:01:54 -0400 Subject: [PATCH 13/49] Changed the name of Eve Fleet Fight Simulator to Eve Fleet Simulator and updated refs to match --- .gitignore | 2 +- effs_stat_export.py => efs_stat_export.py | 14 ++++++------ gui/copySelectDialog.py | 6 ++--- gui/mainFrame.py | 8 +++---- ...ort_all_fits.py => efs_export_all_fits.py} | 0 ...t_base_fits.py => efs_export_base_fits.py} | 18 ++++++++++----- ...t_pyfa_fits.py => efs_export_pyfa_fits.py} | 2 +- ...l_export.py => efs_process_html_export.py} | 4 ++-- savedata/{effs_util.py => efs_util.py} | 8 +++---- savedata/makeAndDiffCheck.sh | 22 +++++++++---------- 10 files changed, 45 insertions(+), 39 deletions(-) rename effs_stat_export.py => efs_stat_export.py (98%) rename savedata/{effs_export_all_fits.py => efs_export_all_fits.py} (100%) rename savedata/{effs_export_base_fits.py => efs_export_base_fits.py} (93%) rename savedata/{effs_export_pyfa_fits.py => efs_export_pyfa_fits.py} (96%) rename savedata/{effs_process_html_export.py => efs_process_html_export.py} (96%) rename savedata/{effs_util.py => efs_util.py} (97%) diff --git a/.gitignore b/.gitignore index e520bee03..a9f2eb124 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -#Fit and ship export data generated by effs_stat_export.py +#Fit and ship export data generated by efs_stat_export.py *JSON.js #Python specific diff --git a/effs_stat_export.py b/efs_stat_export.py similarity index 98% rename from effs_stat_export.py rename to efs_stat_export.py index e723f5485..a8e50f7f4 100755 --- a/effs_stat_export.py +++ b/efs_stat_export.py @@ -543,10 +543,10 @@ def getShipSize(groupID): def parseNeededFitDetails(fit, groupID): includeShipTypeData = groupID > 0 fitID = fit.ID - if len(fit.modules) > 0: - fitName = fit.ship.name + ': ' + fit.name - else: + if includeShipTypeData: fitName = fit.name + else: + fitName = fit.ship.name + ': ' + fit.name print('') print('name: ' + fit.name) fitL = Fit.getInstance() @@ -599,9 +599,9 @@ def parseNeededFitDetails(fit, groupID): 'rigSlots': fitModAttr['rigSlots'], 'lowSlots': fitModAttr['lowSlots'], 'midSlots': fitModAttr['medSlots'], 'highSlots': fitModAttr['hiSlots'], 'turretSlots': fitModAttr['turretSlotsLeft'], 'launcherSlots': fitModAttr['launcherSlotsLeft'], - 'powerOutput': fitModAttr['powerOutput'], 'rigSize': fitModAttr['rigSize'], - 'effectiveTurrets': effectiveTurretSlots, 'effectiveLaunchers': effectiveLauncherSlots, - 'effectiveDroneBandwidth': effectiveDroneBandwidth, + 'powerOutput': fitModAttr['powerOutput'], 'cpuOutput': fitModAttr['cpuOutput'], + 'rigSize': fitModAttr['rigSize'], 'effectiveTurrets': effectiveTurretSlots, + 'effectiveLaunchers': effectiveLauncherSlots, 'effectiveDroneBandwidth': effectiveDroneBandwidth, 'resonance': resonance, 'typeID': fit.shipID, 'groupID': groupID, 'shipSize': shipSize, 'droneControlRange': fitModAttr['droneControlRange'], 'mass': fitModAttr['mass'], 'moduleNames': moduleNames, 'projections': projections, @@ -611,6 +611,6 @@ def parseNeededFitDetails(fit, groupID): except TypeError: print('Error parsing fit:' + str(fit)) print(TypeError) - parsable = {'name': fitName + 'Fit could not be correctly parsed'} + dataDict = {'name': fitName + 'Fit could not be correctly parsed'} export = json.dumps(dataDict, skipkeys=True) return export diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index c1e784853..0c6fe35a2 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -29,21 +29,21 @@ class CopySelectDialog(wx.Dialog): copyFormatDna = 3 copyFormatEsi = 4 copyFormatMultiBuy = 5 - copyFormatEffs = 6 + 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", "CREST", "MultiBuy", "EFFS"] + copyFormats = ["EFT", "EFT (Implants)", "XML", "DNA", "CREST", "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.copyFormatEffs: u"EFFS json stats format"} + CopySelectDialog.copyFormatEfs: u"EFS json stats format"} 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 4b7366e4a..3458cd6d0 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -79,7 +79,7 @@ from eos.db.saveddata.queries import getFit as db_getFit from service.port import Port, IPortUser from service.settings import HTMLExportSettings -from effs_stat_export import parseNeededFitDetails as exportEffsStats +from efs_stat_export import parseNeededFitDetails as exportEfsStats from time import gmtime, strftime @@ -728,9 +728,9 @@ class MainFrame(wx.Frame): fit = db_getFit(self.getActiveFit()) toClipboard(Port.exportMultiBuy(fit)) - def clipboardEffs(self): + def clipboardEfs(self): fit = db_getFit(self.getActiveFit()) - toClipboard(exportEffsStats(fit, 0)) + toClipboard(exportEfsStats(fit, 0)) def importFromClipboard(self, event): clipboard = fromClipboard() @@ -748,7 +748,7 @@ class MainFrame(wx.Frame): CopySelectDialog.copyFormatDna: self.clipboardDna, CopySelectDialog.copyFormatEsi: self.clipboardEsi, CopySelectDialog.copyFormatMultiBuy: self.clipboardMultiBuy, - CopySelectDialog.copyFormatEffs: self.clipboardEffs} + CopySelectDialog.copyFormatEfs: self.clipboardEfs} dlg = CopySelectDialog(self) dlg.ShowModal() selected = dlg.GetSelected() diff --git a/savedata/effs_export_all_fits.py b/savedata/efs_export_all_fits.py similarity index 100% rename from savedata/effs_export_all_fits.py rename to savedata/efs_export_all_fits.py diff --git a/savedata/effs_export_base_fits.py b/savedata/efs_export_base_fits.py similarity index 93% rename from savedata/effs_export_base_fits.py rename to savedata/efs_export_base_fits.py index c08c6c003..a4fa8404d 100644 --- a/savedata/effs_export_base_fits.py +++ b/savedata/efs_export_base_fits.py @@ -78,7 +78,7 @@ eos.db.saveddata_meta.create_all() import json from service.fit import Fit -from effs_stat_export import parseNeededFitDetails +from efs_stat_export import parseNeededFitDetails from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, Table from sqlalchemy.orm import relation, mapper, synonym, deferred @@ -140,7 +140,11 @@ def t3dGetStatSet(dnaString, shipName, groupID, raceID): n = 0 while n < len(t3dModes): dna = dnaString + ':' + str(t3dModes[n].ID) + ';1' - shipModeData += setFitFromString(dna, t3dModes[n].name, groupID) + ',\n' + #Don't add the new line for the last mode + if n < len(t3dModes) - 1: + shipModeData += setFitFromString(dna, t3dModes[n].name, groupID) + ',\n' + else: + shipModeData += setFitFromString(dna, t3dModes[n].name, groupID) n += 1 return shipModeData @@ -168,8 +172,11 @@ def t3cGetStatSet(dnaString, shipName, groupID, raceID): dna = dnaString + ':' + str(ss[0][a].ID) \ + ';1:' + str(ss[1][b].ID) + ';1:' + str(ss[2][c].ID) \ + ';1:' + str(ss[3][d].ID) + ';1' - name = shipName + str(a) + str(b) + str(c) + str(d) - shipPermutationData += setFitFromString(dna, name, groupID) + ',\n' + #Don't add the new line for the last permutation + if a == 2 and b == 2 and c == 2 and d == 2: + shipPermutationData += setFitFromString(dna, shipName, groupID) + else: + shipPermutationData += setFitFromString(dna, shipName, groupID) + ',\n' d += 1 n += 1 c += 1 @@ -193,7 +200,6 @@ def setFitFromString(dnaString, fitName, groupID) : print('Cannot find correct link fits for base calculations') return '' modArray = dnaString.split(':') - additionalModeFit = '' fitL = Fit() fitID = fitL.newFit(int(modArray[0]), fitName) fit = eos.db.getFit(fitID) @@ -246,4 +252,4 @@ def setFitFromString(dnaString, fitName, groupID) : fitL.addCommandFit(fit.ID, infoLinkShip) jsonStr = parseNeededFitDetails(fit, groupID) Fit.deleteFit(fitID) - return jsonStr + additionalModeFit + return jsonStr diff --git a/savedata/effs_export_pyfa_fits.py b/savedata/efs_export_pyfa_fits.py similarity index 96% rename from savedata/effs_export_pyfa_fits.py rename to savedata/efs_export_pyfa_fits.py index 82949f1c4..749fdc1d4 100644 --- a/savedata/effs_export_pyfa_fits.py +++ b/savedata/efs_export_pyfa_fits.py @@ -19,7 +19,7 @@ import eos.db if not os.path.exists(config.savePath): os.mkdir(config.savePath) -from effs_stat_export import parseNeededFitDetails +from efs_stat_export import parseNeededFitDetails def exportPyfaFits(opts): nameReq = '' diff --git a/savedata/effs_process_html_export.py b/savedata/efs_process_html_export.py similarity index 96% rename from savedata/effs_process_html_export.py rename to savedata/efs_process_html_export.py index 5d89a4547..8e48bda3c 100644 --- a/savedata/effs_process_html_export.py +++ b/savedata/efs_process_html_export.py @@ -1,6 +1,6 @@ -from effs_export_base_fits import * +from efs_export_base_fits import * -def effsFitsFromHTMLExport(opts): +def efsFitsFromHTMLExport(opts): if opts: if opts.outputpath: basePath = opts.outputpath diff --git a/savedata/effs_util.py b/savedata/efs_util.py similarity index 97% rename from savedata/effs_util.py rename to savedata/efs_util.py index 93276d281..e52ee82bd 100644 --- a/savedata/effs_util.py +++ b/savedata/efs_util.py @@ -38,16 +38,16 @@ parser.add_option( (options, args) = parser.parse_args() if options.exportfits: - from effs_export_pyfa_fits import exportPyfaFits + from efs_export_pyfa_fits import exportPyfaFits exportPyfaFits(options) if options.exportbaseships: - from effs_export_base_fits import exportBaseShips + from efs_export_base_fits import exportBaseShips exportBaseShips(options) if options.convertfitsfromhtml: - from effs_process_html_export import effsFitsFromHTMLExport - effsFitsFromHTMLExport(options) + from efs_process_html_export import efsFitsFromHTMLExport + efsFitsFromHTMLExport(options) #stuff bellow this point is purely scrap diagnostic stuff and should not be public (as it's scrawl) def printGroupData(): diff --git a/savedata/makeAndDiffCheck.sh b/savedata/makeAndDiffCheck.sh index 6b3bb7640..84ddd89a5 100755 --- a/savedata/makeAndDiffCheck.sh +++ b/savedata/makeAndDiffCheck.sh @@ -16,34 +16,34 @@ else fi if [[ $1 == -f ]] ; then if [[ $MUTE == TRUE ]] ; then - python3opt savedata/effs_util.py\ -f | grep awgahwogfa + python3opt savedata/efs_util.py\ -f | grep awgahwogfa else - python3opt savedata/effs_util.py\ -f\ --search=$4 + python3opt savedata/efs_util.py\ -f\ --search=$4 fi elif [[ $1 == -b ]] ; then if [[ $MUTE == TRUE ]] ; then - python3opt savedata/effs_util.py\ -b | grep awgahwogfa + python3opt savedata/efs_util.py\ -b | grep awgahwogfa else - python3opt savedata/effs_util.py\ -b\ --search=$4 + python3opt savedata/efs_util.py\ -b\ --search=$4 fi elif [[ $1 == -u ]] ; then if [[ $MUTE == TRUE ]] ; then - python3opt savedata/effs_util.py\ -b\ -f\ -o\ .. | grep awgahwogfa + python3opt savedata/efs_util.py\ -b\ -f\ -o\ .. | grep awgahwogfa else - python3opt savedata/effs_util.py\ -b\ -f\ -o\ .. + python3opt savedata/efs_util.py\ -b\ -f\ -o\ .. fi elif [[ $1 == -a ]] ; then if [[ $MUTE == TRUE ]] ; then - python3opt savedata/effs_util.py\ -b\ -f | grep awgahwogfa + python3opt savedata/efs_util.py\ -b\ -f | grep awgahwogfa else - python3opt savedata/effs_util.py\ -b\ -f\ --search=$4 + python3opt savedata/efs_util.py\ -b\ -f\ --search=$4 fi else echo Defaulting to fits and base ships.\n if [[ $MUTE == TRUE ]] ; then - python3opt savedata/effs_util.py\ -b\ -f | grep awgahwogfa + python3opt savedata/efs_util.py\ -b\ -f | grep awgahwogfa else - python3opt savedata/effs_util.py\ -b\ -f\ --search=$4 + python3opt savedata/efs_util.py\ -b\ -f\ --search=$4 fi fi if [[ $EXPECTERRORS == True ]] ; then @@ -51,5 +51,5 @@ if [[ $EXPECTERRORS == True ]] ; then else diff -s --color=always ../shipJSON.js ~/.pyfa/shipJSON.js | grep -m 3 --color '' diff -s --color=always ../shipBaseJSON.js ~/.pyfa/shipBaseJSON.js | grep -m 3 --color '' -/home/stock/scripts/Pyfa/.tox/pep8/bin/flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py,floatspin.py --ignore=E121,E126,E127,E128,E203,E731,F401,E722,E741 effs_stat_export.py --max-line-length=165 +/home/stock/scripts/Pyfa/.tox/pep8/bin/flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py,floatspin.py --ignore=E121,E126,E127,E128,E203,E731,F401,E722,E741 efs_stat_export.py --max-line-length=165 fi From 7a078f433a0facaa769e577befd78a38b2b66382 Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Wed, 11 Jul 2018 12:05:57 -0400 Subject: [PATCH 14/49] draft of misc patches for main branch --- eos/db/saveddata/fit.py | 2 +- eos/effects/skillbonusdronedurability.py | 2 +- eos/effects/skillbonusdroneinterfacing.py | 2 +- eos/gamedata.py | 2 +- .../pyfaGeneralPreferences.py | 10 ++ gui/characterSelection.py | 138 +++++++++++++++++- gui/itemStats.py | 2 +- service/fit.py | 6 +- 8 files changed, 152 insertions(+), 12 deletions(-) diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 07375dcaf..0c34d5279 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -56,7 +56,7 @@ fits_table = Table("fits", saveddata_meta, Column("booster", Boolean, nullable=False, index=True, default=0), Column("targetResistsID", ForeignKey("targetResists.ID"), nullable=True), Column("modeID", Integer, nullable=True), - Column("implantLocation", Integer, nullable=False, default=ImplantLocation.FIT), + Column("implantLocation", Integer, nullable=True), Column("notes", String, nullable=True), Column("ignoreRestrictions", Boolean, default=0), Column("created", DateTime, nullable=True, default=datetime.datetime.now), diff --git a/eos/effects/skillbonusdronedurability.py b/eos/effects/skillbonusdronedurability.py index 1023c8412..f36e82a3f 100644 --- a/eos/effects/skillbonusdronedurability.py +++ b/eos/effects/skillbonusdronedurability.py @@ -6,7 +6,7 @@ type = "passive" def handler(fit, src, context): - lvl = src.level + lvl = src.level if "skill" in context else 1 fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "hp", src.getModifiedItemAttr("hullHpBonus") * lvl) fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "armorHP", diff --git a/eos/effects/skillbonusdroneinterfacing.py b/eos/effects/skillbonusdroneinterfacing.py index 4aec63e83..d832d432b 100644 --- a/eos/effects/skillbonusdroneinterfacing.py +++ b/eos/effects/skillbonusdroneinterfacing.py @@ -6,7 +6,7 @@ type = "passive" def handler(fit, src, context): - lvl = src.level + lvl = src.level if "skill" in context else 1 fit.drones.filteredItemBoost(lambda mod: mod.item.requiresSkill("Drones"), "damageMultiplier", src.getModifiedItemAttr("damageMultiplierBonus") * lvl) fit.fighters.filteredItemBoost(lambda mod: mod.item.requiresSkill("Fighters"), diff --git a/eos/gamedata.py b/eos/gamedata.py index 342a4caf4..243f71bad 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -592,7 +592,7 @@ class Unit(EqBase): lambda d: d * 1000.0, lambda u: u), "Boolean": ( - lambda v, u: "Yes" if v == 1 else "No", + lambda v: "Yes" if v == 1 else "No", lambda d: 1.0 if d == "Yes" else 0.0, lambda u: ""), "typeID": ( diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index 412e1a988..6dc9b8ecb 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -34,6 +34,10 @@ class PFGeneralPref(PreferenceView): self.m_staticline1 = wx.StaticLine(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) mainSizer.Add(self.m_staticline1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) + self.cbDefaultCharImplants = wx.CheckBox(panel, wx.ID_ANY, "Have new fits default to character implants", + wx.DefaultPosition, wx.DefaultSize, 0) + mainSizer.Add(self.cbDefaultCharImplants, 0, wx.ALL | wx.EXPAND, 5) + self.cbGlobalChar = wx.CheckBox(panel, wx.ID_ANY, "Use global character", wx.DefaultPosition, wx.DefaultSize, 0) mainSizer.Add(self.cbGlobalChar, 0, wx.ALL | wx.EXPAND, 5) @@ -118,6 +122,7 @@ class PFGeneralPref(PreferenceView): self.sFit = Fit.getInstance() + self.cbDefaultCharImplants.SetValue(self.sFit.serviceFittingOptions["useCharecterImplantsByDefault"]) self.cbGlobalChar.SetValue(self.sFit.serviceFittingOptions["useGlobalCharacter"]) self.cbGlobalDmgPattern.SetValue(self.sFit.serviceFittingOptions["useGlobalDamagePattern"]) self.cbFitColorSlots.SetValue(self.sFit.serviceFittingOptions["colorFitBySlot"] or False) @@ -135,6 +140,7 @@ class PFGeneralPref(PreferenceView): self.cbShowShipBrowserTooltip.SetValue(self.sFit.serviceFittingOptions["showShipBrowserTooltip"]) self.intDelay.SetValue(self.sFit.serviceFittingOptions["marketSearchDelay"]) + self.cbDefaultCharImplants.Bind(wx.EVT_CHECKBOX, self.OnCBDefaultCharImplantsStateChange) self.cbGlobalChar.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalCharStateChange) self.cbGlobalDmgPattern.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalDmgPatternStateChange) self.cbFitColorSlots.Bind(wx.EVT_CHECKBOX, self.onCBGlobalColorBySlot) @@ -183,6 +189,10 @@ class PFGeneralPref(PreferenceView): wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) event.Skip() + def OnCBDefaultCharImplantsStateChange(self, event): + self.sFit.serviceFittingOptions["useCharecterImplantsByDefault"] = self.cbDefaultCharImplants.GetValue() + event.Skip() + def OnCBGlobalCharStateChange(self, event): self.sFit.serviceFittingOptions["useGlobalCharacter"] = self.cbGlobalChar.GetValue() event.Skip() diff --git a/gui/characterSelection.py b/gui/characterSelection.py index 051ce69b4..b6b034a4c 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -45,8 +45,15 @@ class CharacterSelection(wx.Panel): # cache current selection to fall back in case we choose to open char editor self.charCache = None - self.charChoice = wx.Choice(self) - mainSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3) + #self.charChoice = wx.Choice(self) + #self.charChoice.Append('blarg') + #self.charChoice = wx.Choice(self, wx.ID_ANY, wx.Point(-1,0), wx.DefaultSize, [], style=0, validator=wx.DefaultValidator, name='welp') + self.charChoice = wx.ComboBox( + self, id=wx.ID_ANY, value="", pos=wx.DefaultPosition, + size=wx.DefaultSize, choices=[], style=wx.CB_READONLY, validator=wx.DefaultValidator, + name='welp' + ) + mainSizer.Add(self.charChoice, 1, wx.ALIGN_RIGHT | wx.RIGHT | wx.LEFT, 3) self.refreshCharacterList() @@ -74,7 +81,8 @@ class CharacterSelection(wx.Panel): self.skillReqsStaticBitmap.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu) - self.Bind(wx.EVT_CHOICE, self.charChanged) + #self.Bind(wx.EVT_CHOICE, self.charChanged) + self.Bind(wx.EVT_COMBOBOX, self.charChanged) self.mainFrame.Bind(GE.CHAR_LIST_UPDATED, self.refreshCharacterList) self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) @@ -120,6 +128,91 @@ class CharacterSelection(wx.Panel): selection = self.charChoice.GetCurrentSelection() return self.charChoice.GetClientData(selection) if selection is not -1 else None + def padChoice(self, ind): + return; + from logbook import Logger + pyfalog = Logger(__name__) + #sChar = Character.getInstance() + #activeChar = self.getActiveCharacter() + #charList = sorted(sChar.getCharacterList(), key=lambda c: (not c.ro, c.name)) + charList = list(self.charChoice.GetItems()) + selection = charList[ind] + maxOverallLength = max(map(lambda c: self.mainFrame.GetTextExtent(c).x, charList)) + maxOverallLength = max(self.mainFrame.GetTextExtent("\u2015 Open Character Editor \u2015").x, maxOverallLength) + summedSizeO = sum([ + self.btnRefresh.GetSize().x if 'btnRefresh' in dir(self) else 0, + self.skillReqsStaticBitmap.GetSize().x if 'skillReqsStaticBitmap' in dir(self) else 0, + self.mainFrame.GetTextExtent("Character: ").x, + self.charChoice.GetSize().x if 'charChoice' in dir(self) \ + and self.charChoice is not None and 'GetSize' in dir(self.charChoice) else 0, + ]) + summedSize = sum([ + self.btnRefresh.GetSize().x if 'btnRefresh' in dir(self) else 0, + self.skillReqsStaticBitmap.GetSize().x if 'skillReqsStaticBitmap' in dir(self) else 0, + self.mainFrame.GetTextExtent("Character: ").x, + self.charCache.GetSize().x if 'charCache' in dir(self) \ + and self.charCache is not None and 'GetSize' in dir(self.charCache) else 0 + ]) + realSize = self.GetSize().x + #sizeGap = summedSize - realSize + sizeGap = self.GetBestVirtualSize().x - realSize + paddedName = selection + + maxFromContent = self.mainFrame.GetTextExtent(paddedName).x + sizeGap + maxLength = min(maxFromContent, maxOverallLength) + paddingOccured = False + while self.mainFrame.GetTextExtent(' ' + paddedName).x <= maxLength: + paddingOccured = True + paddedName = ' ' + paddedName + charIDRef = int(self.charChoice.GetClientData(ind)) + pyfalog.error('wwwww') + pyfalog.error(paddedName) + pyfalog.error(self.mainFrame.GetTextExtent(paddedName).x) + pyfalog.error(maxFromContent) + pyfalog.error(maxOverallLength) + pyfalog.error('\n') + pyfalog.error(self.charChoice.GetContainingSizer().GetSize().x) + pyfalog.error(realSize) + pyfalog.error(self.GetBestSize().x) + pyfalog.error(self.GetBestVirtualSize().x) + pyfalog.error('\n') + pyfalog.error(summedSizeO) + pyfalog.error(summedSize) + pyfalog.error( + self.charChoice.GetSize().x if 'charChoice' in dir(self) \ + and self.charChoice is not None and 'GetSize' in dir(self.charChoice) else 0 + ) + pyfalog.error( + self.charCache.GetSize().x if 'charCache' in dir(self) \ + and self.charCache is not None and 'GetSize' in dir(self.charCache) else 0 + ) + pyfalog.error(charIDRef) + pyfalog.error(ind) + pyfalog.error(list(self.charChoice.GetItems())) + ### + import re + for i in range(len(self.charChoice.GetItems())): + origStr = list(self.charChoice.GetItems())[i] + idStore = int(self.charChoice.GetClientData(i)) + self.charChoice.Delete(i) + trimmedStr = '' + for n in range(len(origStr)): + if trimmedStr is not '' or origStr[n] != ' ': + trimmedStr += origStr[n] + possibleName = trimmedStr#re.split(" *", origStr) + pyfalog.error('uuu') + pyfalog.error(possibleName) + self.charChoice.Insert(possibleName, i, idStore) + + if paddingOccured: + self.charChoice.Delete(ind) + pyfalog.error(list(self.charChoice.GetItems())) + self.charChoice.Insert(paddedName, ind, charIDRef) + self.charChoice.Select(ind) + pyfalog.error(list(self.charChoice.GetItems())) + pyfalog.error(int(self.charChoice.GetClientData(ind))) + pyfalog.error('wwwww') + def refreshCharacterList(self, event=None): choice = self.charChoice sChar = Character.getInstance() @@ -128,10 +221,30 @@ class CharacterSelection(wx.Panel): choice.Clear() charList = sorted(sChar.getCharacterList(), key=lambda c: (not c.ro, c.name)) picked = False - + from logbook import Logger + pyfalog = Logger(__name__) + maxOverallLength = max(map(lambda c: self.mainFrame.GetTextExtent(c.name).x, charList)) + maxOverallLength = max(self.mainFrame.GetTextExtent("\u2015 Open Character Editor \u2015").x, maxOverallLength) + #summedSize = sum([ + # self.btnRefresh.GetSize().x if 'btnRefresh' in dir(self) else 0, + # self.skillReqsStaticBitmap.GetSize().x if 'skillReqsStaticBitmap' in dir(self) else 0, + # self.mainFrame.GetTextExtent("Character: ").x, + # self.charCache.GetSize().x if 'charCache' in dir(self) and self.charCache is not None else 0, + # ]) + #realSize = self.GetSize().x + #sizeGap = summedSize - realSize for char in charList: - currId = choice.Append(char.name, char.ID) + paddedName = char.name + #maxFromContent = self.mainFrame.GetTextExtent(paddedName).x + self.mainFrame.GetTextExtent("Character: ").x + #maxFromContent = self.mainFrame.GetTextExtent(paddedName).x + sizeGap + #maxLength = min(maxFromContent, maxOverallLength) + #while self.mainFrame.GetTextExtent(paddedName).x < maxLength: + # paddedName = ' ' + paddedName + #currId = choice.Append(str(maxFromContent) + ' ' + \ + # str(self.mainFrame.GetTextExtent(paddedName).x) + ' ' + str(maxLength), char.ID) + currId = choice.Append(paddedName, char.ID) if char.ID == activeChar: + self.padChoice(currId) choice.SetSelection(currId) self.charChanged(None) picked = True @@ -173,16 +286,27 @@ class CharacterSelection(wx.Panel): if charID == -1: # revert to previous character + self.padChoice(self.charCache) + pyfalog.error('GGG') self.charChoice.SetSelection(self.charCache) + pyfalog.error('GGG') self.mainFrame.showCharacterEditor(event) + pyfalog.error('GGG') return - + self.padChoice(self.charChoice.GetCurrentSelection()) + fitID = self.mainFrame.getActiveFit() + charID = self.getActiveCharacter() + pyfalog.error(self.getActiveCharacter()) self.toggleRefreshButton() + pyfalog.error('RRR') sFit = Fit.getInstance() sFit.changeChar(fitID, charID) self.charCache = self.charChoice.GetCurrentSelection() + pyfalog.error('RRR') + pyfalog.error(fitID) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + pyfalog.error('RRR') def toggleRefreshButton(self): charID = self.getActiveCharacter() @@ -199,6 +323,8 @@ class CharacterSelection(wx.Panel): for i in range(numItems): id_ = choice.GetClientData(i) if id_ == charID: + print((list(choice.GetItems())[0])) + self.padChoice(i) choice.SetSelection(i) return True diff --git a/gui/itemStats.py b/gui/itemStats.py index 6a75b543d..3768780e2 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -102,7 +102,7 @@ class ItemStatsDialog(wx.Dialog): if "wxGTK" in wx.PlatformInfo: self.closeBtn = wx.Button(self, wx.ID_ANY, "Close", wx.DefaultPosition, wx.DefaultSize, 0) self.mainSizer.Add(self.closeBtn, 0, wx.ALL | wx.ALIGN_RIGHT, 5) - self.closeBtn.Bind(wx.EVT_BUTTON, self.closeEvent) + self.closeBtn.Bind(wx.EVT_BUTTON, (lambda e: self.Close())) self.SetSizer(self.mainSizer) diff --git a/service/fit.py b/service/fit.py index dccb58e5e..4772f87af 100644 --- a/service/fit.py +++ b/service/fit.py @@ -33,7 +33,7 @@ from eos.saveddata.fighter import Fighter as es_Fighter from eos.saveddata.implant import Implant as es_Implant from eos.saveddata.ship import Ship as es_Ship from eos.saveddata.module import Module as es_Module, State, Slot -from eos.saveddata.fit import Fit as FitType +from eos.saveddata.fit import Fit as FitType, ImplantLocation from service.character import Character from service.damagePattern import DamagePattern from service.settings import SettingsProvider @@ -60,6 +60,7 @@ class Fit(object): self.dirtyFitIDs = set() serviceFittingDefaultOptions = { + "useCharecterImplantsByDefault": True, "useGlobalCharacter": False, "useGlobalDamagePattern": False, "defaultCharacter": self.character.ID, @@ -147,6 +148,9 @@ class Fit(object): fit.targetResists = self.targetResists fit.character = self.character fit.booster = self.booster + fit.implantLocation = ImplantLocation.CHARACTER if\ + self.serviceFittingOptions["useCharecterImplantsByDefault"] else\ + ImplantLocation.FIT eos.db.save(fit) self.recalc(fit) return fit.ID From f624528ce5579a5d2c5b84503917c634c1a2d38f Mon Sep 17 00:00:00 2001 From: Maru Maru Date: Wed, 11 Jul 2018 02:50:50 -0400 Subject: [PATCH 15/49] trivial line movement --- .../pyfaGeneralPreferences.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index 6dc9b8ecb..920ee2654 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -34,14 +34,14 @@ class PFGeneralPref(PreferenceView): self.m_staticline1 = wx.StaticLine(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) mainSizer.Add(self.m_staticline1, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) - self.cbDefaultCharImplants = wx.CheckBox(panel, wx.ID_ANY, "Have new fits default to character implants", - wx.DefaultPosition, wx.DefaultSize, 0) - mainSizer.Add(self.cbDefaultCharImplants, 0, wx.ALL | wx.EXPAND, 5) - self.cbGlobalChar = wx.CheckBox(panel, wx.ID_ANY, "Use global character", wx.DefaultPosition, wx.DefaultSize, 0) mainSizer.Add(self.cbGlobalChar, 0, wx.ALL | wx.EXPAND, 5) + self.cbDefaultCharImplants = wx.CheckBox(panel, wx.ID_ANY, "Use character implants by default for new fits", + wx.DefaultPosition, wx.DefaultSize, 0) + mainSizer.Add(self.cbDefaultCharImplants, 0, wx.ALL | wx.EXPAND, 5) + self.cbGlobalDmgPattern = wx.CheckBox(panel, wx.ID_ANY, "Use global damage pattern", wx.DefaultPosition, wx.DefaultSize, 0) mainSizer.Add(self.cbGlobalDmgPattern, 0, wx.ALL | wx.EXPAND, 5) @@ -122,8 +122,8 @@ class PFGeneralPref(PreferenceView): self.sFit = Fit.getInstance() - self.cbDefaultCharImplants.SetValue(self.sFit.serviceFittingOptions["useCharecterImplantsByDefault"]) self.cbGlobalChar.SetValue(self.sFit.serviceFittingOptions["useGlobalCharacter"]) + self.cbDefaultCharImplants.SetValue(self.sFit.serviceFittingOptions["useCharecterImplantsByDefault"]) self.cbGlobalDmgPattern.SetValue(self.sFit.serviceFittingOptions["useGlobalDamagePattern"]) self.cbFitColorSlots.SetValue(self.sFit.serviceFittingOptions["colorFitBySlot"] or False) self.cbRackSlots.SetValue(self.sFit.serviceFittingOptions["rackSlots"] or False) @@ -140,8 +140,8 @@ class PFGeneralPref(PreferenceView): self.cbShowShipBrowserTooltip.SetValue(self.sFit.serviceFittingOptions["showShipBrowserTooltip"]) self.intDelay.SetValue(self.sFit.serviceFittingOptions["marketSearchDelay"]) - self.cbDefaultCharImplants.Bind(wx.EVT_CHECKBOX, self.OnCBDefaultCharImplantsStateChange) self.cbGlobalChar.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalCharStateChange) + self.cbDefaultCharImplants.Bind(wx.EVT_CHECKBOX, self.OnCBDefaultCharImplantsStateChange) self.cbGlobalDmgPattern.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalDmgPatternStateChange) self.cbFitColorSlots.Bind(wx.EVT_CHECKBOX, self.onCBGlobalColorBySlot) self.cbRackSlots.Bind(wx.EVT_CHECKBOX, self.onCBGlobalRackSlots) @@ -189,14 +189,14 @@ class PFGeneralPref(PreferenceView): wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) event.Skip() - def OnCBDefaultCharImplantsStateChange(self, event): - self.sFit.serviceFittingOptions["useCharecterImplantsByDefault"] = self.cbDefaultCharImplants.GetValue() - event.Skip() - def OnCBGlobalCharStateChange(self, event): self.sFit.serviceFittingOptions["useGlobalCharacter"] = self.cbGlobalChar.GetValue() event.Skip() + def OnCBDefaultCharImplantsStateChange(self, event): + self.sFit.serviceFittingOptions["useCharecterImplantsByDefault"] = self.cbDefaultCharImplants.GetValue() + event.Skip() + def OnCBGlobalDmgPatternStateChange(self, event): self.sFit.serviceFittingOptions["useGlobalDamagePattern"] = self.cbGlobalDmgPattern.GetValue() event.Skip() From 4be78db73807d2a5ebc5308d68e1c736d7fb64a2 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Mon, 16 Jul 2018 06:25:56 -0400 Subject: [PATCH 16/49] Added additional data to efs exports moduleNames and moved it to service.efsPort --- gui/mainFrame.py | 3 +-- savedata/efs_export_base_fits.py | 2 +- savedata/efs_export_pyfa_fits.py | 2 +- savedata/makeAndDiffCheck.sh | 2 +- efs_stat_export.py => service/efsPort.py | 18 +++++++++++++++++- 5 files changed, 21 insertions(+), 6 deletions(-) rename efs_stat_export.py => service/efsPort.py (97%) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 3458cd6d0..dc0162d69 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -77,10 +77,9 @@ 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 parseNeededFitDetails as exportEfsStats from service.settings import HTMLExportSettings -from efs_stat_export import parseNeededFitDetails as exportEfsStats - from time import gmtime, strftime import threading diff --git a/savedata/efs_export_base_fits.py b/savedata/efs_export_base_fits.py index a4fa8404d..9ebb026d8 100644 --- a/savedata/efs_export_base_fits.py +++ b/savedata/efs_export_base_fits.py @@ -78,7 +78,7 @@ eos.db.saveddata_meta.create_all() import json from service.fit import Fit -from efs_stat_export import parseNeededFitDetails +from service.efsPort import parseNeededFitDetails from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, Table from sqlalchemy.orm import relation, mapper, synonym, deferred diff --git a/savedata/efs_export_pyfa_fits.py b/savedata/efs_export_pyfa_fits.py index 749fdc1d4..5484b097d 100644 --- a/savedata/efs_export_pyfa_fits.py +++ b/savedata/efs_export_pyfa_fits.py @@ -19,7 +19,7 @@ import eos.db if not os.path.exists(config.savePath): os.mkdir(config.savePath) -from efs_stat_export import parseNeededFitDetails +from service.efsPort import parseNeededFitDetails def exportPyfaFits(opts): nameReq = '' diff --git a/savedata/makeAndDiffCheck.sh b/savedata/makeAndDiffCheck.sh index 84ddd89a5..c1aee9dc3 100755 --- a/savedata/makeAndDiffCheck.sh +++ b/savedata/makeAndDiffCheck.sh @@ -51,5 +51,5 @@ if [[ $EXPECTERRORS == True ]] ; then else diff -s --color=always ../shipJSON.js ~/.pyfa/shipJSON.js | grep -m 3 --color '' diff -s --color=always ../shipBaseJSON.js ~/.pyfa/shipBaseJSON.js | grep -m 3 --color '' -/home/stock/scripts/Pyfa/.tox/pep8/bin/flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py,floatspin.py --ignore=E121,E126,E127,E128,E203,E731,F401,E722,E741 efs_stat_export.py --max-line-length=165 +/home/stock/scripts/Pyfa/.tox/pep8/bin/flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py,floatspin.py --ignore=E121,E126,E127,E128,E203,E731,F401,E722,E741 service/efsPort.py --max-line-length=165 fi diff --git a/efs_stat_export.py b/service/efsPort.py similarity index 97% rename from efs_stat_export.py rename to service/efsPort.py index a8e50f7f4..a5d47ad41 100755 --- a/efs_stat_export.py +++ b/service/efsPort.py @@ -247,11 +247,27 @@ def getModuleNames(fit): moduleNames.append('Implants:') for implant in fit.implants: moduleNames.append(implant.item.name) + if len(fit.boosters) > 0: + moduleNames.append('') + moduleNames.append('Boosters:') + for booster in fit.boosters: + moduleNames.append(booster.item.name) if len(fit.commandFits) > 0: moduleNames.append('') moduleNames.append('Command Fits:') for commandFit in fit.commandFits: moduleNames.append(commandFit.name) + if len(fit.projectedModules) > 0: + moduleNames.append('') + moduleNames.append('Projected Modules:') + for mod in fit.projectedModules: + moduleNames.append(mod.item.name) + + if fit.character.name != "All 5": + moduleNames.append('') + moduleNames.append('Character:') + moduleNames.append(fit.character.name) + return moduleNames @@ -595,7 +611,7 @@ def parseNeededFitDetails(fit, groupID): 'maxTargetRange': fit.maxTargetRange, 'scanStrength': fit.scanStrength, 'weaponDPS': fit.weaponDPS, 'alignTime': fit.alignTime, 'signatureRadius': fitModAttr['signatureRadius'], 'weapons': weaponSystems, 'scanRes': fitModAttr['scanResolution'], - 'projectedModules': fit.projectedModules, 'capUsed': fit.capUsed, 'capRecharge': fit.capRecharge, + 'capUsed': fit.capUsed, 'capRecharge': fit.capRecharge, 'rigSlots': fitModAttr['rigSlots'], 'lowSlots': fitModAttr['lowSlots'], 'midSlots': fitModAttr['medSlots'], 'highSlots': fitModAttr['hiSlots'], 'turretSlots': fitModAttr['turretSlotsLeft'], 'launcherSlots': fitModAttr['launcherSlotsLeft'], From b1f1db1bee65ff63df8d96a795d4750accdba088 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Tue, 17 Jul 2018 02:45:00 -0400 Subject: [PATCH 17/49] Change service.efsPort to a class structure for consistancy. --- gui/mainFrame.py | 4 +- savedata/efs_export_base_fits.py | 4 +- savedata/efs_export_pyfa_fits.py | 4 +- service/efsPort.py | 1145 +++++++++++++++--------------- 4 files changed, 576 insertions(+), 581 deletions(-) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index dc0162d69..9bd02feee 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -77,7 +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 parseNeededFitDetails as exportEfsStats +from service.efsPort import EfsPort from service.settings import HTMLExportSettings from time import gmtime, strftime @@ -729,7 +729,7 @@ class MainFrame(wx.Frame): def clipboardEfs(self): fit = db_getFit(self.getActiveFit()) - toClipboard(exportEfsStats(fit, 0)) + toClipboard(EfsPort.exportEfs(fit, 0)) def importFromClipboard(self, event): clipboard = fromClipboard() diff --git a/savedata/efs_export_base_fits.py b/savedata/efs_export_base_fits.py index 9ebb026d8..1682ab716 100644 --- a/savedata/efs_export_base_fits.py +++ b/savedata/efs_export_base_fits.py @@ -78,7 +78,7 @@ eos.db.saveddata_meta.create_all() import json from service.fit import Fit -from service.efsPort import parseNeededFitDetails +from service.efsPort import EfsPort from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, Table from sqlalchemy.orm import relation, mapper, synonym, deferred @@ -250,6 +250,6 @@ def setFitFromString(dnaString, fitName, groupID) : fitL.addCommandFit(fit.ID, shieldLinkShip) fitL.addCommandFit(fit.ID, skirmishLinkShip) fitL.addCommandFit(fit.ID, infoLinkShip) - jsonStr = parseNeededFitDetails(fit, groupID) + jsonStr = EfsPort.exportEfs(fit, groupID) Fit.deleteFit(fitID) return jsonStr diff --git a/savedata/efs_export_pyfa_fits.py b/savedata/efs_export_pyfa_fits.py index 5484b097d..9388f8ad6 100644 --- a/savedata/efs_export_pyfa_fits.py +++ b/savedata/efs_export_pyfa_fits.py @@ -19,7 +19,7 @@ import eos.db if not os.path.exists(config.savePath): os.mkdir(config.savePath) -from service.efsPort import parseNeededFitDetails +from service.efsPort import EfsPort def exportPyfaFits(opts): nameReq = '' @@ -48,7 +48,7 @@ def exportPyfaFits(opts): n += 1 name = fit.ship.name + ': ' + fit.name if n >= skipTill and nameReq in name: - stats = parseNeededFitDetails(fit, 0) + stats = EfsPort.exportEfs(fit, 0) output.write(stats) output.write(',\n') output.write(']);\nexport {shipJSON};') diff --git a/service/efsPort.py b/service/efsPort.py index a5d47ad41..25f6fffa3 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -4,11 +4,10 @@ import platform import re import sys import traceback -from math import log - +import json import eos.db -import json +from math import log from service.fit import Fit from service.market import Market from eos.enum import Enum @@ -17,616 +16,612 @@ 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__) eos.db.saveddata_meta.create_all() class RigSize(Enum): - # Matches to item attribute 'rigSize' on ship and rig items + # Matches to item attribute "rigSize" on ship and rig items SMALL = 1 MEDIUM = 2 LARGE = 3 CAPITAL = 4 -def attrDirectMap(values, target, source): - for val in values: - target[val] = source.itemModifiedAttributes[val] +class EfsPort(): + wepTestSet = {} + @staticmethod + def attrDirectMap(values, target, source): + for val in values: + target[val] = source.itemModifiedAttributes[val] -def getT2MwdSpeed(fit, fitL): - fitID = fit.ID - propID = None - shipHasMedSlots = fit.ship.itemModifiedAttributes['medSlots'] > 0 - shipPower = fit.ship.itemModifiedAttributes['powerOutput'] - # Monitors have a 99% reduction to prop mod power requirements - if fit.ship.name == 'Monitor': - shipPower *= 100 - rigSize = fit.ship.itemModifiedAttributes['rigSize'] - if not shipHasMedSlots: - return None + @staticmethod + def getT2MwdSpeed(fit, fitL): + fitID = fit.ID + propID = None + shipHasMedSlots = fit.ship.itemModifiedAttributes["medSlots"] > 0 + shipPower = fit.ship.itemModifiedAttributes["powerOutput"] + # Monitors have a 99% reduction to prop mod power requirements + if fit.ship.name == "Monitor": + shipPower *= 100 + rigSize = fit.ship.itemModifiedAttributes["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'] + 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 - fitL.appendModule(fitID, propID) - fitL.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 - fitL.removeModule(fitID, mwdPosition) - fitL.recalc(fit) - fit = eos.db.getFit(fitID) - return mwdPropSpeed - - -def getPropData(fit, fitL): - fitID = fit.ID - propGroupId = getGroup('Propulsion Module').ID - propMods = filter(lambda mod: mod.item and mod.item.groupID == propGroupId, 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 = 0 + if propID is None: + return None + fitL.appendModule(fitID, propID) fitL.recalc(fit) fit = eos.db.getFit(fitID) - sp = fit.maxSpeed - sig = fit.ship.itemModifiedAttributes['signatureRadius'] - propWithBloom.state = oldPropState + mwdPropSpeed = fit.maxSpeed + mwdPosition = list(filter(lambda mod: mod.item and mod.item.ID == propID, fit.modules))[0].position + fitL.removeModule(fitID, mwdPosition) fitL.recalc(fit) fit = eos.db.getFit(fitID) - return {'usingMWD': True, 'unpropedSpeed': sp, 'unpropedSig': sig} - return { - 'usingMWD': False, - 'unpropedSpeed': fit.maxSpeed, - 'unpropedSig': fit.ship.itemModifiedAttributes['signatureRadius'] - } + return mwdPropSpeed - -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' - ] - modGroupIds = list(map(lambda s: getGroup(s).ID, modGroupNames)) - modGroupData = dict(map(lambda name, gid: (name, {'name': name, 'id': gid}), - modGroupNames, modGroupIds)) - projectedMods = list(filter(lambda mod: mod.item and mod.item.groupID in modGroupIds, fit.modules)) - projections = [] - for mod in projectedMods: - stats = {} - if mod.item.groupID in [modGroupData['Stasis Web']['id'], modGroupData['Stasis Grappler']['id']]: - stats['type'] = 'Stasis Web' - stats['optimal'] = mod.itemModifiedAttributes['maxRange'] - attrDirectMap(['duration', 'speedFactor'], stats, mod) - elif mod.item.groupID == modGroupData['Weapon Disruptor']['id']: - stats['type'] = 'Weapon Disruptor' - stats['optimal'] = mod.itemModifiedAttributes['maxRange'] - stats['falloff'] = mod.itemModifiedAttributes['falloffEffectiveness'] - attrDirectMap([ - 'trackingSpeedBonus', 'maxRangeBonus', 'falloffBonus', 'aoeCloudSizeBonus', - 'aoeVelocityBonus', 'missileVelocityBonus', 'explosionDelayBonus' - ], stats, mod) - elif mod.item.groupID == modGroupData['Energy Nosferatu']['id']: - stats['type'] = 'Energy Nosferatu' - attrDirectMap(['powerTransferAmount', 'energyNeutralizerSignatureResolution'], stats, mod) - elif mod.item.groupID == modGroupData['Energy Neutralizer']['id']: - stats['type'] = 'Energy Neutralizer' - attrDirectMap([ - 'energyNeutralizerSignatureResolution', 'entityCapacitorLevelModifierSmall', - 'entityCapacitorLevelModifierMedium', 'entityCapacitorLevelModifierLarge', - 'energyNeutralizerAmount' - ], stats, mod) - elif mod.item.groupID in [modGroupData['Remote Shield Booster']['id'], - modGroupData['Ancillary Remote Shield Booster']['id']]: - stats['type'] = 'Remote Shield Booster' - attrDirectMap(['shieldBonus'], stats, mod) - elif mod.item.groupID in [modGroupData['Remote Armor Repairer']['id'], - modGroupData['Ancillary Remote Armor Repairer']['id']]: - stats['type'] = 'Remote Armor Repairer' - attrDirectMap(['armorDamageAmount'], stats, mod) - elif mod.item.groupID == modGroupData['Warp Scrambler']['id']: - stats['type'] = 'Warp Scrambler' - attrDirectMap(['activationBlockedStrenght', 'warpScrambleStrength'], stats, mod) - elif mod.item.groupID == modGroupData['Target Painter']['id']: - stats['type'] = 'Target Painter' - attrDirectMap(['signatureRadiusBonus'], stats, mod) - elif mod.item.groupID == modGroupData['Sensor Dampener']['id']: - stats['type'] = 'Sensor Dampener' - attrDirectMap(['maxTargetRangeBonus', 'scanResolutionBonus'], stats, mod) - elif mod.item.groupID == modGroupData['ECM']['id']: - stats['type'] = 'ECM' - attrDirectMap([ - 'scanGravimetricStrengthBonus', 'scanMagnetometricStrengthBonus', - 'scanRadarStrengthBonus', 'scanLadarStrengthBonus', - ], stats, mod) - elif mod.item.groupID == modGroupData['Burst Jammer']['id']: - stats['type'] = 'Burst Jammer' - mod.itemModifiedAttributes['maxRange'] = mod.itemModifiedAttributes['ecmBurstRange'] - attrDirectMap([ - 'scanGravimetricStrengthBonus', 'scanMagnetometricStrengthBonus', - 'scanRadarStrengthBonus', 'scanLadarStrengthBonus', - ], stats, mod) - elif mod.item.groupID == modGroupData['Micro Jump Drive']['id']: - stats['type'] = 'Micro Jump Drive' - mod.itemModifiedAttributes['maxRange'] = 0 - attrDirectMap(['moduleReactivationDelay'], stats, mod) - if mod.itemModifiedAttributes['maxRange'] is None: - print(mod.item.name) - print(mod.itemModifiedAttributes.items()) - raise ValueError('Projected module lacks a maxRange') - stats['optimal'] = mod.itemModifiedAttributes['maxRange'] - stats['falloff'] = mod.itemModifiedAttributes['falloffEffectiveness'] or 0 - attrDirectMap(['duration', 'capacitorNeed'], stats, mod) - projections.append(stats) - return projections - - -def getModuleNames(fit): - moduleNames = [] - highSlotNames = [] - midSlotNames = [] - lowSlotNames = [] - rigSlotNames = [] - miscSlotNames = [] # subsystems ect - for mod in fit.modules: - if mod.slot == 3: - modSlotNames = highSlotNames - elif mod.slot == 2: - modSlotNames = midSlotNames - elif mod.slot == 1: - modSlotNames = lowSlotNames - elif mod.slot == 4: - modSlotNames = rigSlotNames - elif mod.slot == 5: - modSlotNames = miscSlotNames - try: - if mod.item is not None: - if mod.charge is not None: - modSlotNames.append(mod.item.name + ': ' + mod.charge.name) - else: - modSlotNames.append(mod.item.name) - else: - modSlotNames.append('Empty Slot') - except: - print(vars(mod)) - print('could not find name for module') - print(fit.modules) - for modInfo in [ - ['High Slots:'], highSlotNames, ['', 'Med Slots:'], midSlotNames, - ['', 'Low Slots:'], lowSlotNames, ['', 'Rig Slots:'], rigSlotNames - ]: - moduleNames.extend(modInfo) - - if len(miscSlotNames) > 0: - moduleNames.append('') - moduleNames.append('Subsystems:') - moduleNames.extend(miscSlotNames) - droneNames = [] - fighterNames = [] - for drone in fit.drones: - if drone.amountActive > 0: - droneNames.append("%s x%s" % (drone.item.name, drone.amount)) - for fighter in fit.fighters: - if fighter.amountActive > 0: - fighterNames.append("%s x%s" % (fighter.item.name, fighter.amountActive)) - if len(droneNames) > 0: - moduleNames.append('') - moduleNames.append('Drones:') - moduleNames.extend(droneNames) - if len(fighterNames) > 0: - moduleNames.append('') - moduleNames.append('Fighters:') - moduleNames.extend(fighterNames) - if len(fit.implants) > 0: - moduleNames.append('') - moduleNames.append('Implants:') - for implant in fit.implants: - moduleNames.append(implant.item.name) - if len(fit.boosters) > 0: - moduleNames.append('') - moduleNames.append('Boosters:') - for booster in fit.boosters: - moduleNames.append(booster.item.name) - if len(fit.commandFits) > 0: - moduleNames.append('') - moduleNames.append('Command Fits:') - for commandFit in fit.commandFits: - moduleNames.append(commandFit.name) - if len(fit.projectedModules) > 0: - moduleNames.append('') - moduleNames.append('Projected Modules:') - for mod in fit.projectedModules: - moduleNames.append(mod.item.name) - - if fit.character.name != "All 5": - moduleNames.append('') - moduleNames.append('Character:') - moduleNames.append(fit.character.name) - - return moduleNames - - -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'], - } - - -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.itemModifiedAttributes['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.chargeModifiedAttributes['maxVelocity'] - explosionDelay = stats.chargeModifiedAttributes['explosionDelay'] - damageReductionFactor = stats.chargeModifiedAttributes['aoeDamageReductionFactor'] - explosionRadius = stats.chargeModifiedAttributes['aoeCloudSize'] - explosionVelocity = stats.chargeModifiedAttributes['aoeVelocity'] - typeing = 'Missile' - name = stats.item.name + ', ' + stats.charge.name - elif stats.hardpoint == Hardpoint.NONE: - aoeFieldRange = stats.itemModifiedAttributes['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 + @staticmethod + def getPropData(fit, fitL): + fitID = fit.ID + propGroupId = getGroup("Propulsion Module").ID + propMods = filter(lambda mod: mod.item and mod.item.groupID == propGroupId, 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 = 0 + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + sp = fit.maxSpeed + sig = fit.ship.itemModifiedAttributes["signatureRadius"] + propWithBloom.state = oldPropState + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + return {"usingMWD": True, "unpropedSpeed": sp, "unpropedSig": sig} + return { + "usingMWD": False, + "unpropedSpeed": fit.maxSpeed, + "unpropedSig": fit.ship.itemModifiedAttributes["signatureRadius"] } - weaponSystems.append(statDict) - for drone in fit.drones: - if drone.dps[0] > 0 and drone.amountActive > 0: - droneAttr = drone.itemModifiedAttributes - # Drones are using the old tracking formula for trackingSpeed. This updates it to match turrets. - newTracking = droneAttr['trackingSpeed'] / (droneAttr['optimalSigRadius'] / 40000) + + @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" + ] + modGroupIds = list(map(lambda s: getGroup(s).ID, modGroupNames)) + modGroupData = dict(map(lambda name, gid: (name, {"name": name, "id": gid}), + modGroupNames, modGroupIds)) + projectedMods = list(filter(lambda mod: mod.item and mod.item.groupID in modGroupIds, fit.modules)) + projections = [] + for mod in projectedMods: + stats = {} + if mod.item.groupID in [modGroupData["Stasis Web"]["id"], modGroupData["Stasis Grappler"]["id"]]: + stats["type"] = "Stasis Web" + stats["optimal"] = mod.itemModifiedAttributes["maxRange"] + EfsPort.attrDirectMap(["duration", "speedFactor"], stats, mod) + elif mod.item.groupID == modGroupData["Weapon Disruptor"]["id"]: + stats["type"] = "Weapon Disruptor" + stats["optimal"] = mod.itemModifiedAttributes["maxRange"] + stats["falloff"] = mod.itemModifiedAttributes["falloffEffectiveness"] + EfsPort.attrDirectMap([ + "trackingSpeedBonus", "maxRangeBonus", "falloffBonus", "aoeCloudSizeBonus", + "aoeVelocityBonus", "missileVelocityBonus", "explosionDelayBonus" + ], stats, mod) + elif mod.item.groupID == modGroupData["Energy Nosferatu"]["id"]: + stats["type"] = "Energy Nosferatu" + EfsPort.attrDirectMap(["powerTransferAmount", "energyNeutralizerSignatureResolution"], stats, mod) + elif mod.item.groupID == modGroupData["Energy Neutralizer"]["id"]: + stats["type"] = "Energy Neutralizer" + EfsPort.attrDirectMap([ + "energyNeutralizerSignatureResolution", "entityCapacitorLevelModifierSmall", + "entityCapacitorLevelModifierMedium", "entityCapacitorLevelModifierLarge", + "energyNeutralizerAmount" + ], stats, mod) + elif mod.item.groupID in [modGroupData["Remote Shield Booster"]["id"], + modGroupData["Ancillary Remote Shield Booster"]["id"]]: + stats["type"] = "Remote Shield Booster" + EfsPort.attrDirectMap(["shieldBonus"], stats, mod) + elif mod.item.groupID in [modGroupData["Remote Armor Repairer"]["id"], + modGroupData["Ancillary Remote Armor Repairer"]["id"]]: + stats["type"] = "Remote Armor Repairer" + EfsPort.attrDirectMap(["armorDamageAmount"], stats, mod) + elif mod.item.groupID == modGroupData["Warp Scrambler"]["id"]: + stats["type"] = "Warp Scrambler" + EfsPort.attrDirectMap(["activationBlockedStrenght", "warpScrambleStrength"], stats, mod) + elif mod.item.groupID == modGroupData["Target Painter"]["id"]: + stats["type"] = "Target Painter" + EfsPort.attrDirectMap(["signatureRadiusBonus"], stats, mod) + elif mod.item.groupID == modGroupData["Sensor Dampener"]["id"]: + stats["type"] = "Sensor Dampener" + EfsPort.attrDirectMap(["maxTargetRangeBonus", "scanResolutionBonus"], stats, mod) + elif mod.item.groupID == modGroupData["ECM"]["id"]: + stats["type"] = "ECM" + EfsPort.attrDirectMap([ + "scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus", + "scanRadarStrengthBonus", "scanLadarStrengthBonus", + ], stats, mod) + elif mod.item.groupID == modGroupData["Burst Jammer"]["id"]: + stats["type"] = "Burst Jammer" + mod.itemModifiedAttributes["maxRange"] = mod.itemModifiedAttributes["ecmBurstRange"] + EfsPort.attrDirectMap([ + "scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus", + "scanRadarStrengthBonus", "scanLadarStrengthBonus", + ], stats, mod) + elif mod.item.groupID == modGroupData["Micro Jump Drive"]["id"]: + stats["type"] = "Micro Jump Drive" + mod.itemModifiedAttributes["maxRange"] = 0 + EfsPort.attrDirectMap(["moduleReactivationDelay"], stats, mod) + if mod.itemModifiedAttributes["maxRange"] is None: + pyfalog.error("Projected module {0} has no maxRange".format(mod.item.name)) + stats["optimal"] = mod.itemModifiedAttributes["maxRange"] or 0 + stats["falloff"] = mod.itemModifiedAttributes["falloffEffectiveness"] or 0 + EfsPort.attrDirectMap(["duration", "capacitorNeed"], stats, mod) + projections.append(stats) + return projections + + @staticmethod + def getModuleNames(fit): + moduleNames = [] + highSlotNames = [] + midSlotNames = [] + lowSlotNames = [] + rigSlotNames = [] + miscSlotNames = [] # subsystems ect + for mod in fit.modules: + if mod.slot == 3: + modSlotNames = highSlotNames + elif mod.slot == 2: + modSlotNames = midSlotNames + elif mod.slot == 1: + modSlotNames = lowSlotNames + elif mod.slot == 4: + modSlotNames = rigSlotNames + elif mod.slot == 5: + modSlotNames = miscSlotNames + try: + if mod.item is not None: + if mod.charge is not None: + modSlotNames.append(mod.item.name + ": " + mod.charge.name) + else: + modSlotNames.append(mod.item.name) + else: + modSlotNames.append("Empty Slot") + except: + pyfalog.error("Could not find name for module {0}".format(vars(mod))) + for modInfo in [ + ["High Slots:"], highSlotNames, ["", "Med Slots:"], midSlotNames, + ["", "Low Slots:"], lowSlotNames, ["", "Rig Slots:"], rigSlotNames + ]: + moduleNames.extend(modInfo) + + if len(miscSlotNames) > 0: + moduleNames.append("") + moduleNames.append("Subsystems:") + moduleNames.extend(miscSlotNames) + droneNames = [] + fighterNames = [] + for drone in fit.drones: + if drone.amountActive > 0: + droneNames.append("%s x%s" % (drone.item.name, drone.amount)) + for fighter in fit.fighters: + if fighter.amountActive > 0: + fighterNames.append("%s x%s" % (fighter.item.name, fighter.amountActive)) + if len(droneNames) > 0: + moduleNames.append("") + moduleNames.append("Drones:") + moduleNames.extend(droneNames) + if len(fighterNames) > 0: + moduleNames.append("") + moduleNames.append("Fighters:") + moduleNames.extend(fighterNames) + if len(fit.implants) > 0: + moduleNames.append("") + moduleNames.append("Implants:") + for implant in fit.implants: + moduleNames.append(implant.item.name) + if len(fit.boosters) > 0: + moduleNames.append("") + moduleNames.append("Boosters:") + for booster in fit.boosters: + moduleNames.append(booster.item.name) + if len(fit.commandFits) > 0: + moduleNames.append("") + moduleNames.append("Command Fits:") + for commandFit in fit.commandFits: + moduleNames.append(commandFit.name) + if len(fit.projectedModules) > 0: + moduleNames.append("") + moduleNames.append("Projected Modules:") + for mod in fit.projectedModules: + moduleNames.append(mod.item.name) + + if fit.character.name != "All 5": + moduleNames.append("") + moduleNames.append("Character:") + moduleNames.append(fit.character.name) + + return moduleNames + + @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.itemModifiedAttributes["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.chargeModifiedAttributes["maxVelocity"] + explosionDelay = stats.chargeModifiedAttributes["explosionDelay"] + damageReductionFactor = stats.chargeModifiedAttributes["aoeDamageReductionFactor"] + explosionRadius = stats.chargeModifiedAttributes["aoeCloudSize"] + explosionVelocity = stats.chargeModifiedAttributes["aoeVelocity"] + typeing = "Missile" + name = stats.item.name + ", " + stats.charge.name + elif stats.hardpoint == Hardpoint.NONE: + aoeFieldRange = stats.itemModifiedAttributes["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': 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] + "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 } weaponSystems.append(statDict) - for fighter in fit.fighters: - if fighter.dps[0] > 0 and fighter.amountActive > 0: - fighterAttr = fighter.itemModifiedAttributes - abilities = [] - if 'fighterAbilityAttackMissileDamageEM' in fighterAttr: - baseRef = 'fighterAbilityAttackMissile' - ability = getFighterAbilityData(fighterAttr, fighter, baseRef) - abilities.append(ability) - if 'fighterAbilityMissilesDamageEM' in fighterAttr: - baseRef = 'fighterAbilityMissiles' - ability = 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 + for drone in fit.drones: + if drone.dps[0] > 0 and drone.amountActive > 0: + droneAttr = drone.itemModifiedAttributes + # 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.itemModifiedAttributes + abilities = [] + if "fighterAbilityAttackMissileDamageEM" in fighterAttr: + baseRef = "fighterAbilityAttackMissile" + ability = EfsPort.getFighterAbilityData(fighterAttr, fighter, baseRef) + abilities.append(ability) + if "fighterAbilityMissilesDamageEM" in fighterAttr: + 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) -wepTestSet = {} + 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] -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) + 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 - 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 wepTestSet.keys(): - return wepTestSet[setType] - else: - wepTestSet[setType] = [] - modSet = wepTestSet[setType] - - if setType == 'drone': - ilist = GetT2ItemsWhere(True, True, 'Drone') + 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: - drone = Drone(item) - drone.amount = 1 - drone.amountActive = 1 - drone.itemModifiedAttributes.parent = drone - modSet.append(drone) + 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) + + mkt = 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.itemModifiedAttributes["chargeGroup1"]).all()) + mods = [mod] + charges = [clist[0]] + if setType == "launcher": + # We don"t want variations of missiles we already have + prevCharges = list(mkt.getVariationsByItems(charges)) + testCharges = [] + for charge in clist: + if charge not in prevCharges: + testCharges.append(charge) + prevCharges += mkt.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 - 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) + @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 - mkt = 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.itemModifiedAttributes['chargeGroup1']).all()) - mods = [mod] - charges = [clist[0]] - if setType == 'launcher': - # We don't want variations of missiles we already have - prevCharges = list(mkt.getVariationsByItems(charges)) - testCharges = [] - for charge in clist: - if charge not in prevCharges: - testCharges.append(charge) - prevCharges += mkt.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 + def getCurrentMultipliers(tf): + fitMultipliers = {} + getDroneMulti = lambda d: sumDamage(d.itemModifiedAttributes) * d.itemModifiedAttributes["damageMultiplier"] + fitMultipliers["drones"] = list(map(getDroneMulti, tf.drones)) + getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == Hardpoint.TURRET, f.modules) + getTurretMulti = lambda mod: mod.itemModifiedAttributes["damageMultiplier"] / mod.cycleTime + fitMultipliers["turrets"] = list(map(getTurretMulti, getFitTurrets(tf))) -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 + getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == Hardpoint.MISSILE, f.modules) + getLauncherMulti = lambda mod: sumDamage(mod.chargeModifiedAttributes) / mod.cycleTime + fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf))) + return fitMultipliers - def getCurrentMultipliers(tf): - fitMultipliers = {} - getDroneMulti = lambda d: sumDamage(d.itemModifiedAttributes) * d.itemModifiedAttributes['damageMultiplier'] - fitMultipliers['drones'] = list(map(getDroneMulti, tf.drones)) - - getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == Hardpoint.TURRET, f.modules) - getTurretMulti = lambda mod: mod.itemModifiedAttributes['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.chargeModifiedAttributes) / mod.cycleTime - fitMultipliers['launchers'] = list(map(getLauncherMulti, getFitLaunchers(tf))) - return fitMultipliers - - multipliers = {'turret': 1, 'launcher': 1, 'droneBandwidth': 1} - drones = getTestSet('drone') - launchers = getTestSet('launcher') - turrets = getTestSet('turret') - for weaponTypeSet in [turrets, launchers, drones]: - for mod in weaponTypeSet: - mod.owner = fit - turrets = list(filter(lambda mod: mod.itemModifiedAttributes['damageMultiplier'], turrets)) - launchers = list(filter(lambda mod: sumDamage(mod.chargeModifiedAttributes), launchers)) - # Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits. - tf = Fit.getInstance() - 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(): + 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.itemModifiedAttributes["damageMultiplier"], turrets)) + launchers = list(filter(lambda mod: sumDamage(mod.chargeModifiedAttributes), launchers)) + # Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits. + tf = Fit.getInstance() + 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, 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(): + 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, 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) - tf.recalc(fit) - return multipliers + 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) + tf.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 -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) - print(sizeNotFoundMsg) - return sizeNotFoundMsg + @staticmethod + def exportEfs(fit, groupID): + includeShipTypeData = groupID > 0 + fitID = fit.ID + if includeShipTypeData: + fitName = fit.name + else: + fitName = fit.ship.name + ": " + fit.name + pyfalog.info("Creating Eve Fleet Simulator data for: " + fit.name) + fitL = Fit.getInstance() + fitL.recalc(fit) + fit = eos.db.getFit(fitID) + fitModAttr = fit.ship.itemModifiedAttributes + propData = EfsPort.getPropData(fit, fitL) + mwdPropSpeed = fit.maxSpeed + if includeShipTypeData: + mwdPropSpeed = EfsPort.getT2MwdSpeed(fit, fitL) + projections = EfsPort.getOutgoingProjectionData(fit) + moduleNames = EfsPort.getModuleNames(fit) + weaponSystems = EfsPort.getWeaponSystemData(fit) - -def parseNeededFitDetails(fit, groupID): - includeShipTypeData = groupID > 0 - fitID = fit.ID - if includeShipTypeData: - fitName = fit.name - else: - fitName = fit.ship.name + ': ' + fit.name - print('') - print('name: ' + fit.name) - fitL = Fit.getInstance() - fitL.recalc(fit) - fit = eos.db.getFit(fitID) - fitModAttr = fit.ship.itemModifiedAttributes - propData = getPropData(fit, fitL) - mwdPropSpeed = fit.maxSpeed - if includeShipTypeData: - mwdPropSpeed = getT2MwdSpeed(fit, fitL) - projections = getOutgoingProjectionData(fit) - moduleNames = getModuleNames(fit) - weaponSystems = 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 = 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 groupID == getGroup('Dreadnought').ID: - 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 = getShipSize(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': groupID, 'shipSize': shipSize, - 'droneControlRange': fitModAttr['droneControlRange'], 'mass': fitModAttr['mass'], - 'moduleNames': moduleNames, 'projections': projections, - 'unpropedSpeed': propData['unpropedSpeed'], 'unpropedSig': propData['unpropedSig'], - 'usingMWD': propData['usingMWD'], 'mwdPropSpeed': mwdPropSpeed + 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 groupID == getGroup("Dreadnought").ID: + effectiveTurretSlots *= 9.4 + effectiveLauncherSlots *= 15 + hullResonance = { + "exp": fitModAttr["explosiveDamageResonance"], "kin": fitModAttr["kineticDamageResonance"], + "therm": fitModAttr["thermalDamageResonance"], "em": fitModAttr["emDamageResonance"] } - except TypeError: - print('Error parsing fit:' + str(fit)) - print(TypeError) - dataDict = {'name': fitName + 'Fit could not be correctly parsed'} - export = json.dumps(dataDict, skipkeys=True) - return export + 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(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": groupID, "shipSize": shipSize, + "droneControlRange": fitModAttr["droneControlRange"], "mass": fitModAttr["mass"], + "moduleNames": moduleNames, "projections": projections, + "unpropedSpeed": propData["unpropedSpeed"], "unpropedSig": propData["unpropedSig"], + "usingMWD": propData["usingMWD"], "mwdPropSpeed": mwdPropSpeed + } + 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 From ebac100e38f5109324c6db4a733512f52070d95a Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Tue, 17 Jul 2018 04:22:40 -0400 Subject: [PATCH 18/49] Adjusted EFS to clipboard tooltip --- gui/copySelectDialog.py | 2 +- service/efsPort.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 7f6ec1da8..b74c93b3f 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -43,7 +43,7 @@ class CopySelectDialog(wx.Dialog): CopySelectDialog.copyFormatDna: "A one-line text format", CopySelectDialog.copyFormatEsi: "A JSON format used for EVE CREST", CopySelectDialog.copyFormatMultiBuy: "MultiBuy text format", - CopySelectDialog.copyFormatEfs: u"EFS json stats 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/service/efsPort.py b/service/efsPort.py index 25f6fffa3..31b9fbf91 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -596,7 +596,7 @@ class EfsPort(): "therm": fitModAttr["shieldThermalDamageResonance"], "em": fitModAttr["shieldEmDamageResonance"] } resonance = {"hull": hullResonance, "armor": armorResonance, "shield": shieldResonance} - shipSize = EfsPort.getShipSize(groupID) + shipSize = EfsPort.getShipSize(fit.ship.item.groupID) try: dataDict = { From c0096fc0163a657c3ab939c3ca778a3bbcc5c8d9 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Tue, 17 Jul 2018 20:46:15 -0400 Subject: [PATCH 19/49] Revert irrelevent changes compared to master --- .gitignore | 3 - gui/characterSelection.py | 138 ++------------------------------------ 2 files changed, 6 insertions(+), 135 deletions(-) diff --git a/.gitignore b/.gitignore index 7dc223640..a9eb5e25c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -#Fit and ship export data generated by efs_stat_export.py -*JSON.js - #Python specific *.pyc diff --git a/gui/characterSelection.py b/gui/characterSelection.py index b6b034a4c..051ce69b4 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -45,15 +45,8 @@ class CharacterSelection(wx.Panel): # cache current selection to fall back in case we choose to open char editor self.charCache = None - #self.charChoice = wx.Choice(self) - #self.charChoice.Append('blarg') - #self.charChoice = wx.Choice(self, wx.ID_ANY, wx.Point(-1,0), wx.DefaultSize, [], style=0, validator=wx.DefaultValidator, name='welp') - self.charChoice = wx.ComboBox( - self, id=wx.ID_ANY, value="", pos=wx.DefaultPosition, - size=wx.DefaultSize, choices=[], style=wx.CB_READONLY, validator=wx.DefaultValidator, - name='welp' - ) - mainSizer.Add(self.charChoice, 1, wx.ALIGN_RIGHT | wx.RIGHT | wx.LEFT, 3) + self.charChoice = wx.Choice(self) + mainSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3) self.refreshCharacterList() @@ -81,8 +74,7 @@ class CharacterSelection(wx.Panel): self.skillReqsStaticBitmap.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu) - #self.Bind(wx.EVT_CHOICE, self.charChanged) - self.Bind(wx.EVT_COMBOBOX, self.charChanged) + self.Bind(wx.EVT_CHOICE, self.charChanged) self.mainFrame.Bind(GE.CHAR_LIST_UPDATED, self.refreshCharacterList) self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) @@ -128,91 +120,6 @@ class CharacterSelection(wx.Panel): selection = self.charChoice.GetCurrentSelection() return self.charChoice.GetClientData(selection) if selection is not -1 else None - def padChoice(self, ind): - return; - from logbook import Logger - pyfalog = Logger(__name__) - #sChar = Character.getInstance() - #activeChar = self.getActiveCharacter() - #charList = sorted(sChar.getCharacterList(), key=lambda c: (not c.ro, c.name)) - charList = list(self.charChoice.GetItems()) - selection = charList[ind] - maxOverallLength = max(map(lambda c: self.mainFrame.GetTextExtent(c).x, charList)) - maxOverallLength = max(self.mainFrame.GetTextExtent("\u2015 Open Character Editor \u2015").x, maxOverallLength) - summedSizeO = sum([ - self.btnRefresh.GetSize().x if 'btnRefresh' in dir(self) else 0, - self.skillReqsStaticBitmap.GetSize().x if 'skillReqsStaticBitmap' in dir(self) else 0, - self.mainFrame.GetTextExtent("Character: ").x, - self.charChoice.GetSize().x if 'charChoice' in dir(self) \ - and self.charChoice is not None and 'GetSize' in dir(self.charChoice) else 0, - ]) - summedSize = sum([ - self.btnRefresh.GetSize().x if 'btnRefresh' in dir(self) else 0, - self.skillReqsStaticBitmap.GetSize().x if 'skillReqsStaticBitmap' in dir(self) else 0, - self.mainFrame.GetTextExtent("Character: ").x, - self.charCache.GetSize().x if 'charCache' in dir(self) \ - and self.charCache is not None and 'GetSize' in dir(self.charCache) else 0 - ]) - realSize = self.GetSize().x - #sizeGap = summedSize - realSize - sizeGap = self.GetBestVirtualSize().x - realSize - paddedName = selection - - maxFromContent = self.mainFrame.GetTextExtent(paddedName).x + sizeGap - maxLength = min(maxFromContent, maxOverallLength) - paddingOccured = False - while self.mainFrame.GetTextExtent(' ' + paddedName).x <= maxLength: - paddingOccured = True - paddedName = ' ' + paddedName - charIDRef = int(self.charChoice.GetClientData(ind)) - pyfalog.error('wwwww') - pyfalog.error(paddedName) - pyfalog.error(self.mainFrame.GetTextExtent(paddedName).x) - pyfalog.error(maxFromContent) - pyfalog.error(maxOverallLength) - pyfalog.error('\n') - pyfalog.error(self.charChoice.GetContainingSizer().GetSize().x) - pyfalog.error(realSize) - pyfalog.error(self.GetBestSize().x) - pyfalog.error(self.GetBestVirtualSize().x) - pyfalog.error('\n') - pyfalog.error(summedSizeO) - pyfalog.error(summedSize) - pyfalog.error( - self.charChoice.GetSize().x if 'charChoice' in dir(self) \ - and self.charChoice is not None and 'GetSize' in dir(self.charChoice) else 0 - ) - pyfalog.error( - self.charCache.GetSize().x if 'charCache' in dir(self) \ - and self.charCache is not None and 'GetSize' in dir(self.charCache) else 0 - ) - pyfalog.error(charIDRef) - pyfalog.error(ind) - pyfalog.error(list(self.charChoice.GetItems())) - ### - import re - for i in range(len(self.charChoice.GetItems())): - origStr = list(self.charChoice.GetItems())[i] - idStore = int(self.charChoice.GetClientData(i)) - self.charChoice.Delete(i) - trimmedStr = '' - for n in range(len(origStr)): - if trimmedStr is not '' or origStr[n] != ' ': - trimmedStr += origStr[n] - possibleName = trimmedStr#re.split(" *", origStr) - pyfalog.error('uuu') - pyfalog.error(possibleName) - self.charChoice.Insert(possibleName, i, idStore) - - if paddingOccured: - self.charChoice.Delete(ind) - pyfalog.error(list(self.charChoice.GetItems())) - self.charChoice.Insert(paddedName, ind, charIDRef) - self.charChoice.Select(ind) - pyfalog.error(list(self.charChoice.GetItems())) - pyfalog.error(int(self.charChoice.GetClientData(ind))) - pyfalog.error('wwwww') - def refreshCharacterList(self, event=None): choice = self.charChoice sChar = Character.getInstance() @@ -221,30 +128,10 @@ class CharacterSelection(wx.Panel): choice.Clear() charList = sorted(sChar.getCharacterList(), key=lambda c: (not c.ro, c.name)) picked = False - from logbook import Logger - pyfalog = Logger(__name__) - maxOverallLength = max(map(lambda c: self.mainFrame.GetTextExtent(c.name).x, charList)) - maxOverallLength = max(self.mainFrame.GetTextExtent("\u2015 Open Character Editor \u2015").x, maxOverallLength) - #summedSize = sum([ - # self.btnRefresh.GetSize().x if 'btnRefresh' in dir(self) else 0, - # self.skillReqsStaticBitmap.GetSize().x if 'skillReqsStaticBitmap' in dir(self) else 0, - # self.mainFrame.GetTextExtent("Character: ").x, - # self.charCache.GetSize().x if 'charCache' in dir(self) and self.charCache is not None else 0, - # ]) - #realSize = self.GetSize().x - #sizeGap = summedSize - realSize + for char in charList: - paddedName = char.name - #maxFromContent = self.mainFrame.GetTextExtent(paddedName).x + self.mainFrame.GetTextExtent("Character: ").x - #maxFromContent = self.mainFrame.GetTextExtent(paddedName).x + sizeGap - #maxLength = min(maxFromContent, maxOverallLength) - #while self.mainFrame.GetTextExtent(paddedName).x < maxLength: - # paddedName = ' ' + paddedName - #currId = choice.Append(str(maxFromContent) + ' ' + \ - # str(self.mainFrame.GetTextExtent(paddedName).x) + ' ' + str(maxLength), char.ID) - currId = choice.Append(paddedName, char.ID) + currId = choice.Append(char.name, char.ID) if char.ID == activeChar: - self.padChoice(currId) choice.SetSelection(currId) self.charChanged(None) picked = True @@ -286,27 +173,16 @@ class CharacterSelection(wx.Panel): if charID == -1: # revert to previous character - self.padChoice(self.charCache) - pyfalog.error('GGG') self.charChoice.SetSelection(self.charCache) - pyfalog.error('GGG') self.mainFrame.showCharacterEditor(event) - pyfalog.error('GGG') return - self.padChoice(self.charChoice.GetCurrentSelection()) - fitID = self.mainFrame.getActiveFit() - charID = self.getActiveCharacter() - pyfalog.error(self.getActiveCharacter()) + self.toggleRefreshButton() - pyfalog.error('RRR') sFit = Fit.getInstance() sFit.changeChar(fitID, charID) self.charCache = self.charChoice.GetCurrentSelection() - pyfalog.error('RRR') - pyfalog.error(fitID) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) - pyfalog.error('RRR') def toggleRefreshButton(self): charID = self.getActiveCharacter() @@ -323,8 +199,6 @@ class CharacterSelection(wx.Panel): for i in range(numItems): id_ = choice.GetClientData(i) if id_ == charID: - print((list(choice.GetItems())[0])) - self.padChoice(i) choice.SetSelection(i) return True From 682607c31f75401ec9d8f83b42c4e13017420502 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Tue, 17 Jul 2018 21:01:26 -0400 Subject: [PATCH 20/49] Remove local build files not intended for git --- savedata/efs_export_all_fits.py | 0 savedata/efs_export_base_fits.py | 255 ---------------------------- savedata/efs_export_pyfa_fits.py | 55 ------ savedata/efs_process_html_export.py | 60 ------- savedata/efs_util.py | 212 ----------------------- savedata/makeAndDiffCheck.sh | 55 ------ 6 files changed, 637 deletions(-) delete mode 100644 savedata/efs_export_all_fits.py delete mode 100644 savedata/efs_export_base_fits.py delete mode 100644 savedata/efs_export_pyfa_fits.py delete mode 100644 savedata/efs_process_html_export.py delete mode 100644 savedata/efs_util.py delete mode 100755 savedata/makeAndDiffCheck.sh diff --git a/savedata/efs_export_all_fits.py b/savedata/efs_export_all_fits.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/savedata/efs_export_base_fits.py b/savedata/efs_export_base_fits.py deleted file mode 100644 index 1682ab716..000000000 --- a/savedata/efs_export_base_fits.py +++ /dev/null @@ -1,255 +0,0 @@ -import inspect -import os -import platform -import re -import sys -import traceback -from optparse import AmbiguousOptionError, BadOptionError, OptionParser - -from logbook import CRITICAL, DEBUG, ERROR, FingersCrossedHandler, INFO, Logger, NestedSetup, NullHandler, StreamHandler, TimedRotatingFileHandler, WARNING, \ - __version__ as logbook_version - -sys.path.append(os.getcwd()) -import config - -from math import log - -try: - import wxversion -except ImportError: - wxversion = None - -try: - import sqlalchemy -except ImportError: - sqlalchemy = None - -pyfalog = Logger(__name__) - -class PassThroughOptionParser(OptionParser): - - def _process_args(self, largs, rargs, values): - while rargs: - try: - OptionParser._process_args(self, largs, rargs, values) - except (BadOptionError, AmbiguousOptionError) as e: - pyfalog.error("Bad startup option passed.") - largs.append(e.opt_str) - -usage = "usage: %prog [--root]" -parser = PassThroughOptionParser(usage=usage) -parser.add_option("-r", "--root", action="store_true", dest="rootsavedata", help="if you want pyfa to store its data in root folder, use this option", default=False) -parser.add_option("-w", "--wx28", action="store_true", dest="force28", help="Force usage of wxPython 2.8", default=False) -parser.add_option("-d", "--debug", action="store_true", dest="debug", help="Set logger to debug level.", default=False) -parser.add_option("-t", "--title", action="store", dest="title", help="Set Window Title", default=None) -parser.add_option("-s", "--savepath", action="store", dest="savepath", help="Set the folder for savedata", default=None) -parser.add_option("-l", "--logginglevel", action="store", dest="logginglevel", help="Set desired logging level [Critical|Error|Warning|Info|Debug]", default="Error") - -(options, args) = parser.parse_args() - -if options.rootsavedata is True: - config.saveInRoot = True - -config.debug = options.debug - -config.defPaths(options.savepath) - -try: - import requests - config.requestsVersion = requests.__version__ -except ImportError: - raise PreCheckException("Cannot import requests. You can download requests from https://pypi.python.org/pypi/requests.") - -import eos.db - -#if config.saVersion[0] > 0 or config.saVersion[1] >= 7: - # <0.7 doesn't have support for events ;_; (mac-deprecated) -config.sa_events = True -import eos.events - - # noinspection PyUnresolvedReferences -import service.prefetch # noqa: F401 - - # Make sure the saveddata db exists -if not os.path.exists(config.savePath): - os.mkdir(config.savePath) - -eos.db.saveddata_meta.create_all() - -import json -from service.fit import Fit -from service.efsPort import EfsPort - -from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, Table -from sqlalchemy.orm import relation, mapper, synonym, deferred -from eos.db import gamedata_session -from eos.db import gamedata_meta -from eos.db.gamedata.metaGroup import metatypes_table, items_table -from eos.db.gamedata.group import groups_table - -from eos.gamedata import AlphaClone, Attribute, Category, Group, Item, MarketGroup, \ - MetaGroup, AttributeInfo, MetaData, Effect, ItemEffect, Traits -from eos.db.gamedata.traits import traits_table -from eos.saveddata.mode import Mode - -def exportBaseShips(opts): - nameReq = '' - if opts: - if opts.search: - nameReq = opts.search - if opts.outputpath: - basePath = opts.outputpath - elif opts.savepath: - basePath = opts.savepath - else: - basePath = config.savePath + os.sep - else: - basePath = config.savePath + os.sep - if basePath[len(basePath) - 1] != os.sep: - basePath = basePath + os.sep - outputBaseline = open(basePath + 'shipBaseJSON.js', 'w') - outputBaseline.write('let shipBaseJSON = JSON.stringify([') - shipCata = eos.db.getItemsByCategory('Ship') - baseLimit = 1000 - baseN = 0 - for ship in iter(shipCata): - if baseN < baseLimit and nameReq in ship.name: - print(ship.name) - print(ship.groupID) - dna = str(ship.ID) - if ship.groupID == 963: - stats = t3cGetStatSet(dna, ship.name, ship.groupID, ship.raceID) - elif ship.groupID == 1305: - stats = t3dGetStatSet(dna, ship.name, ship.groupID, ship.raceID) - else: - stats = setFitFromString(dna, ship.name, ship.groupID) - outputBaseline.write(stats) - outputBaseline.write(',\n') - baseN += 1 - outputBaseline.write(']);\nexport {shipBaseJSON};') - outputBaseline.close() - -def t3dGetStatSet(dnaString, shipName, groupID, raceID): - t3dModeGroupFilter = Group.groupID == 1306 - data = list(gamedata_session.query(Group).options().filter(t3dModeGroupFilter).all()) - #Normally we would filter this via the raceID, - #Unfortunately somebody fat fingered the Jackdaw modes raceIDs as 4 (Amarr) not 1 (Caldari) - # t3dModes = list(filter(lambda mode: mode.raceID == raceID, data[0].items)) #Line for if/when they fix it - t3dModes = list(filter(lambda mode: shipName in mode.name, data[0].items)) - shipModeData = '' - n = 0 - while n < len(t3dModes): - dna = dnaString + ':' + str(t3dModes[n].ID) + ';1' - #Don't add the new line for the last mode - if n < len(t3dModes) - 1: - shipModeData += setFitFromString(dna, t3dModes[n].name, groupID) + ',\n' - else: - shipModeData += setFitFromString(dna, t3dModes[n].name, groupID) - n += 1 - return shipModeData - -def t3cGetStatSet(dnaString, shipName, groupID, raceID): - subsystemFilter = Group.categoryID == 32 - data = list(gamedata_session.query(Group).options().filter(subsystemFilter).all()) - # multi dimension array to hold the t3c subsystems as ss[index of subsystem type][index subsystem item] - ss = [[], [], [], []] - s = 0 - while s < 4: - ss[s] = list(filter(lambda subsystem: subsystem.raceID == raceID, data[s].items)) - s += 1 - print(shipName) - print(ss) - shipPermutationData = '' - n = 0 - a = 0 - while a < 3: - b = 0 - while b < 3: - c = 0 - while c < 3: - d = 0 - while d < 3: - dna = dnaString + ':' + str(ss[0][a].ID) \ - + ';1:' + str(ss[1][b].ID) + ';1:' + str(ss[2][c].ID) \ - + ';1:' + str(ss[3][d].ID) + ';1' - #Don't add the new line for the last permutation - if a == 2 and b == 2 and c == 2 and d == 2: - shipPermutationData += setFitFromString(dna, shipName, groupID) - else: - shipPermutationData += setFitFromString(dna, shipName, groupID) + ',\n' - d += 1 - n += 1 - c += 1 - b += 1 - a += 1 - print(str(n) + ' subsystem conbinations for ' + shipName) - return shipPermutationData -try: - armorLinkShip = eos.db.searchFits('armor links')[0] - infoLinkShip = eos.db.searchFits('information links')[0] - shieldLinkShip = eos.db.searchFits('shield links')[0] - skirmishLinkShip = eos.db.searchFits('skirmish links')[0] -except: - armorLinkShip = None - infoLinkShip = None - shieldLinkShip = None - skirmishLinkShip = None - -def setFitFromString(dnaString, fitName, groupID) : - if armorLinkShip == None: - print('Cannot find correct link fits for base calculations') - return '' - modArray = dnaString.split(':') - fitL = Fit() - fitID = fitL.newFit(int(modArray[0]), fitName) - fit = eos.db.getFit(fitID) - ammoArray = [] - n = -1 - for mod in iter(modArray): - n = n + 1 - if n > 0: - modSp = mod.split(';') - if len(modSp) == 2: - k = 0 - while k < int(modSp[1]): - k = k + 1 - itemID = int(modSp[0]) - item = eos.db.getItem(int(modSp[0]), eager=("attributes", "group.category")) - cat = item.category.name - print(cat) - if cat == 'Drone': - fitL.addDrone(fitID, itemID, int(modSp[1]), recalc=False) - k += int(modSp[1]) - if cat == 'Fighter': - fitL.addFighter(fitID, itemID, recalc=False) - k += 100 - if fitL.isAmmo(int(modSp[0])): - k += 100 - ammoArray.append(int(modSp[0])); - # Set mode if module is a mode on a t3d - if item.groupID == 1306 and groupID == 1305: - fitL.setMode(fitID, Mode(item)) - else: - fitL.appendModule(fitID, int(modSp[0])) - fit = eos.db.getFit(fitID) - for ammo in iter(ammoArray): - fitL.setAmmo(fitID, ammo, list(filter(lambda mod: str(mod).find('name') > 0, fit.modules))) - if len(fit.drones) > 0: - fit.drones[0].amountActive = fit.drones[0].amount - eos.db.commit() - for fighter in iter(fit.fighters): - for ability in fighter.abilities: - if ability.effect.handlerName == u'fighterabilityattackm' and ability.active == True: - for abilityAltRef in fighter.abilities: - if abilityAltRef.effect.isImplemented: - abilityAltRef.active = True - fitL.recalc(fit) - fit = eos.db.getFit(fitID) - print(list(filter(lambda mod: mod.item and mod.item.groupID in [1189, 658], fit.modules))) - fitL.addCommandFit(fit.ID, armorLinkShip) - fitL.addCommandFit(fit.ID, shieldLinkShip) - fitL.addCommandFit(fit.ID, skirmishLinkShip) - fitL.addCommandFit(fit.ID, infoLinkShip) - jsonStr = EfsPort.exportEfs(fit, groupID) - Fit.deleteFit(fitID) - return jsonStr diff --git a/savedata/efs_export_pyfa_fits.py b/savedata/efs_export_pyfa_fits.py deleted file mode 100644 index 9388f8ad6..000000000 --- a/savedata/efs_export_pyfa_fits.py +++ /dev/null @@ -1,55 +0,0 @@ -import inspect -import os -import platform -import re -import sys -import traceback - -sys.path.append(os.getcwd()) -import config -from pyfa import options - -if options.rootsavedata is True: - config.saveInRoot = True -config.debug = options.debug -config.defPaths(options.savepath) - -import eos.db -# Make sure the saveddata db exists -if not os.path.exists(config.savePath): - os.mkdir(config.savePath) - -from service.efsPort import EfsPort - -def exportPyfaFits(opts): - nameReq = '' - if opts: - if opts.search: - nameReq = opts.search - if opts.outputpath: - basePath = opts.outputpath - elif opts.savepath: - basePath = opts.savepath - else: - basePath = config.savePath + os.sep - else: - basePath = config.savePath + os.sep - if basePath[len(basePath) - 1] != os.sep: - basePath = basePath + os.sep - output = open(basePath + 'shipJSON.js', 'w') - output.write('let shipJSON = JSON.stringify([') - #The current storage system isn't going to hold more than 2500 fits as local browser storage is limited - limit = 2500 - skipTill = 0 - n = 0 - fitList = eos.db.getFitList() - for fit in fitList: - if limit == None or n < limit: - n += 1 - name = fit.ship.name + ': ' + fit.name - if n >= skipTill and nameReq in name: - stats = EfsPort.exportEfs(fit, 0) - output.write(stats) - output.write(',\n') - output.write(']);\nexport {shipJSON};') - output.close() diff --git a/savedata/efs_process_html_export.py b/savedata/efs_process_html_export.py deleted file mode 100644 index 8e48bda3c..000000000 --- a/savedata/efs_process_html_export.py +++ /dev/null @@ -1,60 +0,0 @@ -from efs_export_base_fits import * - -def efsFitsFromHTMLExport(opts): - if opts: - if opts.outputpath: - basePath = opts.outputpath - elif opts.savepath: - basePath = opts.savepath - else: - basePath = config.savePath + os.sep - else: - basePath = config.savePath + os.sep - if basePath[len(basePath) - 1] != os.sep: - basePath = basePath + os.sep - output = open(basePath + 'shipJSON.js', 'w') - output.write('let shipJSON = JSON.stringify([') - try: - with open('pyfaFits.html'): - fileLocation = 'pyfaFits.html' - except: - try: - d = config.savePath + os.sep + 'pyfaFits.html' - print(d) - with open(d): - fileLocation = d - except: - fileLocation = None; - limit = 10000 - n = 0 - skipTill = 0 - nameReq = '' - minimalExport = True - if fileLocation != None: - with open(fileLocation) as f: - for fullLine in f: - if limit == None or n < limit: - if n <= 1 and '' in fullLine: - minimalExport = False - n += 1 - fullIndex = fullLine.find('data-dna="') - minimalIndex = fullLine.find('/dna/') - if fullIndex >= 0: - startInd = fullLine.find('data-dna="') + 10 - elif minimalIndex >= 0 and minimalExport: - startInd = fullLine.find('/dna/') + 5 - else: - startInd = -1 - print(startInd) - if startInd >= 0: - line = fullLine[startInd:len(fullLine)] - endInd = line.find('::') - dna = line[0:endInd] - name = line[line.find('>') + 1:line.find('<')] - if n >= skipTill and nameReq in name: - print('name: ' + name + ' DNA: ' + dna + fullLine) - stats = setFitFromString(dna, name, 0) - output.write(stats) - output.write(',\n') - output.write(']);\nexport {shipJSON};') - output.close() diff --git a/savedata/efs_util.py b/savedata/efs_util.py deleted file mode 100644 index e52ee82bd..000000000 --- a/savedata/efs_util.py +++ /dev/null @@ -1,212 +0,0 @@ -from optparse import AmbiguousOptionError, BadOptionError, OptionParser - -class PassThroughOptionParser(OptionParser): - - def _process_args(self, largs, rargs, values): - while rargs: - try: - OptionParser._process_args(self, largs, rargs, values) - except (BadOptionError, AmbiguousOptionError) as e: - pyfalog.error("Bad startup option passed.") - largs.append(e.opt_str) - -usage = "usage: %prog [options]" -parser = PassThroughOptionParser(usage=usage) -parser.add_option( - "-f", "--exportfits", action="store_true", dest="exportfits", \ - help="Export this copy of pyfa's local fits to a shipJSON file that Eve Fleet Simulator can import from", \ - default=False) -parser.add_option( - "-b", "--exportbaseships", action="store_true", dest="exportbaseships", \ - help="Export ship stats to a shipBaseJSON file used by Eve Fleet Simulator", \ - default=False) -parser.add_option( - "-c", "--convertfitsfromhtml", action="store_true", dest="convertfitsfromhtml", \ - help="Convert an exported pyfaFits.html file to a shipJSON file that Eve Fleet Simulator can import from\n" - + " Note this process loses data like fleet boosters as the DNA format exported by to html contains limited data", \ - default=False) -parser.add_option("-s", "--savepath", action="store", dest="savepath", - help="Set the folder for savedata", default=None) -parser.add_option( - "-o", "--outputpath", action="store", dest="outputpath", - help="Output directory, defaults to the savepath", default=None) -parser.add_option( - '-i', "--search", action="store", dest="search", - help="Ignore ships and fits that don't contain the searched string", default=None) - - -(options, args) = parser.parse_args() - -if options.exportfits: - from efs_export_pyfa_fits import exportPyfaFits - exportPyfaFits(options) - -if options.exportbaseships: - from efs_export_base_fits import exportBaseShips - exportBaseShips(options) - -if options.convertfitsfromhtml: - from efs_process_html_export import efsFitsFromHTMLExport - efsFitsFromHTMLExport(options) - -#stuff bellow this point is purely scrap diagnostic stuff and should not be public (as it's scrawl) -def printGroupData(): - from eos.db import gamedata_session - from eos.gamedata import Group, Category - filterVal = Group.categoryID == 6 - data = gamedata_session.query(Group).options().list(filter(filterVal).all()) - for group in data: - print(group.groupName + ' groupID: ' + str(group.groupID)) - return '' - -def printSizeData(): - from eos.db import gamedata_session - from eos.gamedata import Group - filterVal = Group.categoryID == 6 - data = gamedata_session.query(Group).options().filter(filterVal).all() - ships = gamedata_session.query(Group).options().filter(filterVal) - print(data) - print(vars(data[0])) - - shipSizes = ['Frigate', 'Destroyer', 'Cruiser', 'Battlecruiser', 'Battleship', 'Capital', 'Industrial', 'Misc'] - groupSets = [ - [25, 31, 237, 324, 830, 831, 834, 893, 1283, 1527], - [420, 541, 1305, 1534], - [26, 358, 832, 833, 894, 906, 963], - [419, 540, 1201], - [27, 381, 898, 900], - [30, 485, 513, 547, 659, 883, 902, 1538], - [28, 380, 1202, 463, 543, 941], - [29, 1022] - ] - i = 0 - while i < 8: - groupNames = '\'' + shipSizes[i] + '\': {\'name\': \'' + shipSizes[i] + '\', \'groupIDs\': groupIDFromGroupName([' - for gid in groupSets[i]: - if gid is not groupSets[i][0]: - groupNames = groupNames + '\', ' - groupNames = groupNames + '\'' + list(filter(lambda gr: gr.groupID == gid, data))[0].groupName - print(groupNames + '\'], data)}') - i = i + 1 - projectedModGroupIds = [ - 41, 52, 65, 67, 68, 71, 80, 201, 208, 291, 325, 379, 585, - 842, 899, 1150, 1154, 1189, 1306, 1672, 1697, 1698, 1815, 1894 - ] - from eos.db import gamedata_session - from eos.gamedata import Group - data = gamedata_session.query(Group).all() - groupNames = '' - for gid in projectedModGroupIds: - if gid is not projectedModGroupIds[0]: - groupNames = groupNames + '\', ' - print(gid) - groupNames = groupNames + '\'' + list(filter(lambda gr: gr.groupID == gid, data))[0].groupName - print(groupNames + '\'') - -def wepMultisFromTraitText(fit): - filterVal = Traits.typeID == fit.shipID - data = gamedata_session.query(Traits).options().filter(filterVal).all() - roleBonusMode = False - if len(data) == 0: - return multipliers - d = data[0] - s1 = str(vars(d)) - ds = s1.encode(encoding="utf-8", errors="ignore") - #print(ds) - previousTypedBonus = 0 - previousDroneTypeBonus = 0 - for bonusText in data[0].traitText.splitlines(): - bonusText = bonusText.lower() - if 'per skill level' in bonusText: - roleBonusMode = False - if 'role bonus' in bonusText or 'misc bonus' in bonusText: - roleBonusMode = True - multi = 1 - if 'damage' in bonusText and not any(e in bonusText for e in ['control', 'heat']): - splitText = bonusText.split('%') - if (float(splitText[0]) > 0) is False: - print('damage bonus split did not parse correctly!') - print(float(splitText[0])) - if roleBonusMode: - addedMulti = float(splitText[0]) - else: - addedMulti = float(splitText[0]) * 5 - if any(e in bonusText for e in [' em', 'thermal', 'kinetic', 'explosive']): - if addedMulti > previousTypedBonus: - previousTypedBonus = addedMulti - else: - addedMulti = 0 - if any(e in bonusText for e in ['heavy drone', 'medium drone', 'light drone', 'sentry drone']): - if addedMulti > previousDroneTypeBonus: - previousDroneTypeBonus = addedMulti - else: - addedMulti = 0 - multi = 1 + (addedMulti / 100) - elif 'rate of fire' in bonusText: - splitText = bonusText.split('%') - if (float(splitText[0]) > 0) is False: - print('rate of fire bonus split did not parse correctly!') - print(float(splitText[0])) - if roleBonusMode: - rofMulti = float(splitText[0]) - else: - rofMulti = float(splitText[0]) * 5 - multi = 1 / (1 - (rofMulti / 100)) - if multi > 1: - if 'drone' in bonusText.lower(): - multipliers['droneBandwidth'] *= multi - elif 'turret' in bonusText.lower(): - multipliers['turret'] *= multi - elif any(e in bonusText for e in ['missile', 'torpedo']): - multipliers['launcher'] *= multi - - -def examDiff(ai, bi, attr=False): - print('') - print('A:' + str(ai)) - print('B:' + str(bi)) - a = dict(map(lambda k: (k, getattr(ai, k)), dir(ai))) - b = dict(map(lambda k: (k, getattr(bi, k)), dir(bi))) - try: - print(a.keys()) - print('A:' + str(a)) - print(b.keys()) - print('B:' + str(b)) - print('A exclusive keys:') - for key in filter(lambda k: k not in b.keys(), a.keys()): - print(key) - print('B exclusive keys:') - for key in filter(lambda k: k not in a.keys(), b.keys()): - print(key) - print('A key/value pairs where B is None:') - for key in filter(lambda k: k in b.keys() and b[k] == None and a[k] != None, a.keys()): - print(key) - print(a[key]) - print('B key/value pairs where A is None:') - for key in filter(lambda k: k in a.keys() and a[k] == None and b[k] != None, b.keys()): - print(key) - print(b[key]) - except Exception as e: - if attr == True: - print('Could not print itemModifiedAttributes for a or b') - print(e) - else: - print('Checking itemModifiedAttributes diff') - examDiff(ai.itemModifiedAttributes, bi.itemModifiedAttributes, True) - if attr == False: - print('Checking itemModifiedAttributes diff') - examDiff(ai.itemModifiedAttributes, bi.itemModifiedAttributes, True) - print('') - -def groupIDFromGroupName(names, data=None): - # Group data can optionally be passed to the function to improve preformace with repeated calls. - if data is None: - data = gamedata_session.query(Group).all() - returnSingle = False - if not isinstance(names, list): - names = [names] - returnSingle = True - gidList = list(map(lambda incGroup: incGroup.groupID, (filter(lambda group: group.groupName in names, data)))) - if returnSingle: - return gidList[0] - return gidList diff --git a/savedata/makeAndDiffCheck.sh b/savedata/makeAndDiffCheck.sh deleted file mode 100755 index c1aee9dc3..000000000 --- a/savedata/makeAndDiffCheck.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -if [[ $2 == -v ]] ; then - MUTE=False -else - MUTE=TRUE -fi -EXPECTERRORS=False -if [[ $3 == --search ]] ; then - if [[ $5 == --expect-errors ]] ; then - EXPECTERRORS=True - fi -else - if [[ $3 == --expect-errors ]] ; then - EXPECTERRORS=True - fi -fi -if [[ $1 == -f ]] ; then - if [[ $MUTE == TRUE ]] ; then - python3opt savedata/efs_util.py\ -f | grep awgahwogfa - else - python3opt savedata/efs_util.py\ -f\ --search=$4 - fi -elif [[ $1 == -b ]] ; then - if [[ $MUTE == TRUE ]] ; then - python3opt savedata/efs_util.py\ -b | grep awgahwogfa - else - python3opt savedata/efs_util.py\ -b\ --search=$4 - fi -elif [[ $1 == -u ]] ; then - if [[ $MUTE == TRUE ]] ; then - python3opt savedata/efs_util.py\ -b\ -f\ -o\ .. | grep awgahwogfa - else - python3opt savedata/efs_util.py\ -b\ -f\ -o\ .. - fi -elif [[ $1 == -a ]] ; then - if [[ $MUTE == TRUE ]] ; then - python3opt savedata/efs_util.py\ -b\ -f | grep awgahwogfa - else - python3opt savedata/efs_util.py\ -b\ -f\ --search=$4 - fi -else - echo Defaulting to fits and base ships.\n - if [[ $MUTE == TRUE ]] ; then - python3opt savedata/efs_util.py\ -b\ -f | grep awgahwogfa - else - python3opt savedata/efs_util.py\ -b\ -f\ --search=$4 - fi -fi -if [[ $EXPECTERRORS == True ]] ; then - echo Expecting non standard output, this should only be used for testing -else -diff -s --color=always ../shipJSON.js ~/.pyfa/shipJSON.js | grep -m 3 --color '' -diff -s --color=always ../shipBaseJSON.js ~/.pyfa/shipBaseJSON.js | grep -m 3 --color '' -/home/stock/scripts/Pyfa/.tox/pep8/bin/flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py,floatspin.py --ignore=E121,E126,E127,E128,E203,E731,F401,E722,E741 service/efsPort.py --max-line-length=165 -fi From a6a0831123746158842cc83011fb023579ddf868 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Tue, 17 Jul 2018 21:03:39 -0400 Subject: [PATCH 21/49] Removed uneeded file --- savedata/getmods.py | 88 --------------------------------------------- 1 file changed, 88 deletions(-) delete mode 100644 savedata/getmods.py diff --git a/savedata/getmods.py b/savedata/getmods.py deleted file mode 100644 index 16ef06103..000000000 --- a/savedata/getmods.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -#!/usr/bin/env python -if __name__ == "__main__": - import argparse - import json - import os.path - import sys - sys.path.append(os.getcwd()) - - from eos import * - from eos.data.data_handler import JsonDataHandler - from eos.const.eos import * - - json_path = r"/path/to/Phobos/dump" - - parser = argparse.ArgumentParser(description="Figure out what actually effect does") - parser.add_argument("-e", "--effect", type=str, required=True, help="effect name") - args = parser.parse_args() - - # open a few files to get human-readable names for data (EOS strictly works with numerical identifiers) - with open(os.path.join(json_path, "dgmattribs.json"), mode='r', encoding="utf8") as file: - dgmattribs = json.load(file) - - with open(os.path.join(json_path, 'dgmeffects.json'), mode='r', encoding="utf8") as file: - dgmeffects = json.load(file) - - with open(os.path.join(json_path, 'invtypes.json'), mode='r', encoding="utf8") as file: - invtypes = json.load(file) - - with open(os.path.join(json_path, 'invgroups.json'), mode='r', encoding="utf8") as file: - invgroups = json.load(file) - - attr_id_name = {} - attr_id_penalized = {} - for row in dgmattribs: - attr_id_name[row['attributeID']] = row['attributeName'] - attr_id_penalized[row['attributeID']] = 'not penalized' if row['stackable'] else 'penalized' - - effect_id_name = {} - for row in dgmeffects: - effect_id_name[row['effectID']] = row['effectName'] - if row['effectName'] == args.effect: - effect_id = row['effectID'] - break - - type_id_name = {} - for _, row in invtypes.items(): - name = row.get("typeName_en-us", None) - if name: - type_id_name[row['typeID']] = name - - group_id_name = {} - for _, row in invgroups.items(): - group_id_name[row['groupID']] = row['groupName_en-us'] - - data_handler = JsonDataHandler(json_path) # Folder with Phobos data dump - cache_handler = JsonCacheHandler(os.path.join(json_path, "cache", "eos_tq.json.bz2")) - SourceManager.add('evedata', data_handler, cache_handler, make_default=True) - - effect = cache_handler.get_effect(effect_id) - modifiers = effect.modifiers - mod_counter = 1 - indent = ' ' - print('effect {}.py (id: {}) - build status is {}'.format(args.effect.lower(), effect_id, EffectBuildStatus(effect.build_status).name)) - for modifier in modifiers: - print('{}Modifier {}:'.format(indent, mod_counter)) - print('{0}{0}state: {1}'.format(indent, State(modifier.state).name)) - print('{0}{0}scope: {1}'.format(indent, Scope(modifier.scope).name)) - print('{0}{0}srcattr: {1} {2}'.format(indent, attr_id_name[modifier.src_attr], modifier.src_attr)) - print('{0}{0}operator: {1} {2}'.format(indent, Operator(modifier.operator).name, modifier.operator)) - print('{0}{0}tgtattr: {1} ({2}) {3}'.format( - indent, - attr_id_name[modifier.tgt_attr], - attr_id_penalized[modifier.tgt_attr],modifier.tgt_attr) - ) - print('{0}{0}location: {1}'.format(indent, Domain(modifier.domain).name)) - try: - filter_type = FilterType(modifier.filter_type).name - except ValueError: - filter_type = None - print('{0}{0}filter type: {1}'.format(indent, filter_type)) - if modifier.filter_type is None or modifier.filter_type in (FilterType.all_, FilterType.skill_self): - pass - elif modifier.filter_type == FilterType.skill: - print('{0}{0}filter value: {1}'.format(indent, type_id_name[modifier.filter_value])) - elif modifier.filter_type == FilterType.group: - print('{0}{0}filter value: {1}'.format(indent, group_id_name[modifier.filter_value])) - mod_counter += 1 From 6be77646fcf063bd3ef39d6d5189d9a556fd9239 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Tue, 17 Jul 2018 22:32:30 -0400 Subject: [PATCH 22/49] Linting for consistancy --- service/efsPort.py | 37 ++++++++++++++++++++----------------- service/fit.py | 1 - 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/service/efsPort.py b/service/efsPort.py index 31b9fbf91..5bc5e5632 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -38,7 +38,7 @@ class EfsPort(): target[val] = source.itemModifiedAttributes[val] @staticmethod - def getT2MwdSpeed(fit, fitL): + def getT2MwdSpeed(fit, sFit): fitID = fit.ID propID = None shipHasMedSlots = fit.ship.itemModifiedAttributes["medSlots"] > 0 @@ -70,18 +70,18 @@ class EfsPort(): if propID is None: return None - fitL.appendModule(fitID, propID) - fitL.recalc(fit) + 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 - fitL.removeModule(fitID, mwdPosition) - fitL.recalc(fit) + sFit.removeModule(fitID, mwdPosition) + sFit.recalc(fit) fit = eos.db.getFit(fitID) return mwdPropSpeed @staticmethod - def getPropData(fit, fitL): + def getPropData(fit, sFit): fitID = fit.ID propGroupId = getGroup("Propulsion Module").ID propMods = filter(lambda mod: mod.item and mod.item.groupID == propGroupId, fit.modules) @@ -90,12 +90,12 @@ class EfsPort(): if propWithBloom is not None: oldPropState = propWithBloom.state propWithBloom.state = 0 - fitL.recalc(fit) + sFit.recalc(fit) fit = eos.db.getFit(fitID) sp = fit.maxSpeed sig = fit.ship.itemModifiedAttributes["signatureRadius"] propWithBloom.state = oldPropState - fitL.recalc(fit) + sFit.recalc(fit) fit = eos.db.getFit(fitID) return {"usingMWD": True, "unpropedSpeed": sp, "unpropedSig": sig} return { @@ -426,7 +426,7 @@ class EfsPort(): mod = Module(item) modSet.append(mod) - mkt = Market.getInstance() + 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: @@ -436,12 +436,12 @@ class EfsPort(): charges = [clist[0]] if setType == "launcher": # We don"t want variations of missiles we already have - prevCharges = list(mkt.getVariationsByItems(charges)) + prevCharges = list(sMkt.getVariationsByItems(charges)) testCharges = [] for charge in clist: if charge not in prevCharges: testCharges.append(charge) - prevCharges += mkt.getVariationsByItems([charge]) + prevCharges += sMkt.getVariationsByItems([charge]) for c in testCharges: charges.append(c) additionalLauncher = Module(mod.item) @@ -488,7 +488,10 @@ class EfsPort(): turrets = list(filter(lambda mod: mod.itemModifiedAttributes["damageMultiplier"], turrets)) launchers = list(filter(lambda mod: sumDamage(mod.chargeModifiedAttributes), launchers)) # Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits. - tf = Fit.getInstance() + # standin class used to prevent . notation causing issues internally + class standin(): + pass + tf = standin() tf.modules = HandledList(turrets + launchers) tf.character = fit.character tf.ship = fit.ship @@ -517,7 +520,7 @@ class EfsPort(): multipliers["turret"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "turrets"), 6) multipliers["launcher"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "launchers"), 6) multipliers["droneBandwidth"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "drones"), 6) - tf.recalc(fit) + Fit.getInstance().recalc(fit) return multipliers @staticmethod @@ -560,14 +563,14 @@ class EfsPort(): else: fitName = fit.ship.name + ": " + fit.name pyfalog.info("Creating Eve Fleet Simulator data for: " + fit.name) - fitL = Fit.getInstance() - fitL.recalc(fit) + sFit = Fit.getInstance() + sFit.recalc(fit) fit = eos.db.getFit(fitID) fitModAttr = fit.ship.itemModifiedAttributes - propData = EfsPort.getPropData(fit, fitL) + propData = EfsPort.getPropData(fit, sFit) mwdPropSpeed = fit.maxSpeed if includeShipTypeData: - mwdPropSpeed = EfsPort.getT2MwdSpeed(fit, fitL) + mwdPropSpeed = EfsPort.getT2MwdSpeed(fit, sFit) projections = EfsPort.getOutgoingProjectionData(fit) moduleNames = EfsPort.getModuleNames(fit) weaponSystems = EfsPort.getWeaponSystemData(fit) diff --git a/service/fit.py b/service/fit.py index 26913846c..122de2fa6 100644 --- a/service/fit.py +++ b/service/fit.py @@ -60,7 +60,6 @@ class Fit(object): self.dirtyFitIDs = set() serviceFittingDefaultOptions = { - "useCharecterImplantsByDefault": True, "useGlobalCharacter": False, "useCharacterImplantsByDefault": True, "useGlobalDamagePattern": False, From 2f7a3e0287123d21293674048a2585a2f56d96af Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Tue, 17 Jul 2018 22:35:09 -0400 Subject: [PATCH 23/49] Have imported fits always use implants if present --- service/port.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 From c054a2d80d0409a21a602dad55d6f7ad8e23cb63 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Tue, 17 Jul 2018 23:09:39 -0400 Subject: [PATCH 24/49] Trivial lint --- service/efsPort.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/efsPort.py b/service/efsPort.py index 5bc5e5632..1b325c6d8 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -488,7 +488,7 @@ class EfsPort(): turrets = list(filter(lambda mod: mod.itemModifiedAttributes["damageMultiplier"], turrets)) launchers = list(filter(lambda mod: sumDamage(mod.chargeModifiedAttributes), 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 internally + # standin class used to prevent . notation causing issues when used as an arg class standin(): pass tf = standin() From 0a8bb79e478c94ae6b3fb54531ccaa6bd14e35f0 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Thu, 19 Jul 2018 13:16:59 -0400 Subject: [PATCH 25/49] Update market.service to reflect new booster market groups --- service/market.py | 38 ++++++++++++++------------------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/service/market.py b/service/market.py index 895cb2e98..9035512d9 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 True and 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 From fe0266e5171ea0371a7db1e0fa0d04e39ce81a0e Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Thu, 19 Jul 2018 05:40:18 -0400 Subject: [PATCH 26/49] Added mitigation for outdated forced groupMarketIDs --- service/market.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/service/market.py b/service/market.py index 9035512d9..545b1a66f 100644 --- a/service/market.py +++ b/service/market.py @@ -527,7 +527,7 @@ class Market(object): categories = ['Drone', 'Fighter', 'Implant'] for item in items: - if True and item.category.ID == 20 and item.group.ID != 303: # Implants not 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 ") @@ -647,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 From 38726675e1f509dcf5c2a5f8fad28da943025118 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Mon, 23 Jul 2018 20:28:53 -0400 Subject: [PATCH 27/49] Added disintegrator stats --- service/efsPort.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/service/efsPort.py b/service/efsPort.py index 1b325c6d8..2c4a4423e 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -332,7 +332,9 @@ class EfsPort(): "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 + "explosionRadius": explosionRadius, "explosionVelocity": explosionVelocity, "aoeFieldRange": aoeFieldRange, + "damageMultiplierBonusMax": stats.getModifiedItemAttr("damageMultiplierBonusMax"), + "damageMultiplierBonusPerCycle": stats.getModifiedItemAttr("damageMultiplierBonusPerCycle") } weaponSystems.append(statDict) for drone in fit.drones: From 03e325cdcbfc646930bc60d11864d27a2dcf0868 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Thu, 26 Jul 2018 11:23:39 -0400 Subject: [PATCH 28/49] Change efsPort.getOutgoingProjectionData to corectly use mod.item.group.name --- service/efsPort.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/service/efsPort.py b/service/efsPort.py index 2c4a4423e..189467cb1 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -116,18 +116,15 @@ class EfsPort(): "Ancillary Remote Shield Booster", "Ancillary Remote Armor Repairer", "Titan Phenomena Generator", "Non-Repeating Hardeners" ] - modGroupIds = list(map(lambda s: getGroup(s).ID, modGroupNames)) - modGroupData = dict(map(lambda name, gid: (name, {"name": name, "id": gid}), - modGroupNames, modGroupIds)) - projectedMods = list(filter(lambda mod: mod.item and mod.item.groupID in modGroupIds, fit.modules)) + projectedMods = list(filter(lambda mod: mod.item and mod.item.group.name in modGroupNames, fit.modules)) projections = [] for mod in projectedMods: stats = {} - if mod.item.groupID in [modGroupData["Stasis Web"]["id"], modGroupData["Stasis Grappler"]["id"]]: + if mod.item.group.name in ["Stasis Web", "Stasis Grappler"]: stats["type"] = "Stasis Web" stats["optimal"] = mod.itemModifiedAttributes["maxRange"] EfsPort.attrDirectMap(["duration", "speedFactor"], stats, mod) - elif mod.item.groupID == modGroupData["Weapon Disruptor"]["id"]: + elif mod.item.group.name == "Weapon Disruptor": stats["type"] = "Weapon Disruptor" stats["optimal"] = mod.itemModifiedAttributes["maxRange"] stats["falloff"] = mod.itemModifiedAttributes["falloffEffectiveness"] @@ -135,50 +132,50 @@ class EfsPort(): "trackingSpeedBonus", "maxRangeBonus", "falloffBonus", "aoeCloudSizeBonus", "aoeVelocityBonus", "missileVelocityBonus", "explosionDelayBonus" ], stats, mod) - elif mod.item.groupID == modGroupData["Energy Nosferatu"]["id"]: + elif mod.item.group.name == "Energy Nosferatu": stats["type"] = "Energy Nosferatu" EfsPort.attrDirectMap(["powerTransferAmount", "energyNeutralizerSignatureResolution"], stats, mod) - elif mod.item.groupID == modGroupData["Energy Neutralizer"]["id"]: + elif mod.item.group.name == "Energy Neutralizer": stats["type"] = "Energy Neutralizer" EfsPort.attrDirectMap([ "energyNeutralizerSignatureResolution", "entityCapacitorLevelModifierSmall", "entityCapacitorLevelModifierMedium", "entityCapacitorLevelModifierLarge", "energyNeutralizerAmount" ], stats, mod) - elif mod.item.groupID in [modGroupData["Remote Shield Booster"]["id"], - modGroupData["Ancillary Remote Shield Booster"]["id"]]: + 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.groupID in [modGroupData["Remote Armor Repairer"]["id"], - modGroupData["Ancillary Remote Armor Repairer"]["id"]]: + 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.groupID == modGroupData["Warp Scrambler"]["id"]: + elif mod.item.group.name == "Warp Scrambler": stats["type"] = "Warp Scrambler" EfsPort.attrDirectMap(["activationBlockedStrenght", "warpScrambleStrength"], stats, mod) - elif mod.item.groupID == modGroupData["Target Painter"]["id"]: + elif mod.item.group.name == "Target Painter": stats["type"] = "Target Painter" EfsPort.attrDirectMap(["signatureRadiusBonus"], stats, mod) - elif mod.item.groupID == modGroupData["Sensor Dampener"]["id"]: + elif mod.item.group.name == "Sensor Dampener": stats["type"] = "Sensor Dampener" EfsPort.attrDirectMap(["maxTargetRangeBonus", "scanResolutionBonus"], stats, mod) - elif mod.item.groupID == modGroupData["ECM"]["id"]: + elif mod.item.group.name == "ECM": stats["type"] = "ECM" EfsPort.attrDirectMap([ "scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus", "scanRadarStrengthBonus", "scanLadarStrengthBonus", ], stats, mod) - elif mod.item.groupID == modGroupData["Burst Jammer"]["id"]: + elif mod.item.group.name == "Burst Jammer": stats["type"] = "Burst Jammer" mod.itemModifiedAttributes["maxRange"] = mod.itemModifiedAttributes["ecmBurstRange"] EfsPort.attrDirectMap([ "scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus", "scanRadarStrengthBonus", "scanLadarStrengthBonus", ], stats, mod) - elif mod.item.groupID == modGroupData["Micro Jump Drive"]["id"]: + elif mod.item.group.name == "Micro Jump Drive": stats["type"] = "Micro Jump Drive" mod.itemModifiedAttributes["maxRange"] = 0 EfsPort.attrDirectMap(["moduleReactivationDelay"], stats, mod) + else: + pyfalog.error("Projected module {0} lacks efs export implementation".format(mod.item.name)) if mod.itemModifiedAttributes["maxRange"] is None: pyfalog.error("Projected module {0} has no maxRange".format(mod.item.name)) stats["optimal"] = mod.itemModifiedAttributes["maxRange"] or 0 @@ -489,6 +486,7 @@ class EfsPort(): mod.owner = fit turrets = list(filter(lambda mod: mod.itemModifiedAttributes["damageMultiplier"], turrets)) launchers = list(filter(lambda mod: sumDamage(mod.chargeModifiedAttributes), 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(): From 582a3893d1004be342b18e7e85626b40bdf947a0 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Thu, 26 Jul 2018 11:48:34 -0400 Subject: [PATCH 29/49] Removed unneeded initalization from efsPort --- service/efsPort.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/service/efsPort.py b/service/efsPort.py index 189467cb1..068fec254 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -18,7 +18,6 @@ from eos.db import gamedata_session, getItemsByCategory, getCategory, getAttribu from eos.gamedata import Category, Group, Item, Traits, Attribute, Effect, ItemEffect from logbook import Logger pyfalog = Logger(__name__) -eos.db.saveddata_meta.create_all() class RigSize(Enum): @@ -555,17 +554,14 @@ class EfsPort(): return sizeNotFoundMsg @staticmethod - def exportEfs(fit, groupID): - includeShipTypeData = groupID > 0 - fitID = fit.ID + 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) - sFit = Fit.getInstance() - sFit.recalc(fit) - fit = eos.db.getFit(fitID) fitModAttr = fit.ship.itemModifiedAttributes propData = EfsPort.getPropData(fit, sFit) mwdPropSpeed = fit.maxSpeed @@ -583,7 +579,7 @@ class EfsPort(): effectiveLauncherSlots = round(launcherSlots * weaponBonusMultipliers["launcher"], 2) effectiveDroneBandwidth = round(droneBandwidth * weaponBonusMultipliers["droneBandwidth"], 2) # Assume a T2 siege module for dreads - if groupID == getGroup("Dreadnought").ID: + if fit.ship.item.groupID == getGroup("Dreadnought").ID: effectiveTurretSlots *= 9.4 effectiveLauncherSlots *= 15 hullResonance = { @@ -616,7 +612,7 @@ class EfsPort(): "powerOutput": fitModAttr["powerOutput"], "cpuOutput": fitModAttr["cpuOutput"], "rigSize": fitModAttr["rigSize"], "effectiveTurrets": effectiveTurretSlots, "effectiveLaunchers": effectiveLauncherSlots, "effectiveDroneBandwidth": effectiveDroneBandwidth, - "resonance": resonance, "typeID": fit.shipID, "groupID": groupID, "shipSize": shipSize, + "resonance": resonance, "typeID": fit.shipID, "groupID": fit.ship.item.groupID, "shipSize": shipSize, "droneControlRange": fitModAttr["droneControlRange"], "mass": fitModAttr["mass"], "moduleNames": moduleNames, "projections": projections, "unpropedSpeed": propData["unpropedSpeed"], "unpropedSig": propData["unpropedSig"], From 4fc630d44e0ad5345a817a287b4098b20e7b5099 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Thu, 26 Jul 2018 12:02:04 -0400 Subject: [PATCH 30/49] Minor efsPort linting --- service/efsPort.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/service/efsPort.py b/service/efsPort.py index 068fec254..9dd236e7a 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -11,7 +11,7 @@ from math import log from service.fit import Fit from service.market import Market from eos.enum import Enum -from eos.saveddata.module import Hardpoint, Slot, Module +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 @@ -82,13 +82,12 @@ class EfsPort(): @staticmethod def getPropData(fit, sFit): fitID = fit.ID - propGroupId = getGroup("Propulsion Module").ID - propMods = filter(lambda mod: mod.item and mod.item.groupID == propGroupId, fit.modules) + 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 = 0 + propWithBloom.state = State.ONLINE sFit.recalc(fit) fit = eos.db.getFit(fitID) sp = fit.maxSpeed From bef8fbbc3aa67e7963bd52a5e3fb0b346e912839 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Thu, 26 Jul 2018 14:24:55 -0400 Subject: [PATCH 31/49] Used getModifiedItemAttr in efsPort to replace itemModifiedAttributes for consistancy --- service/efsPort.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/service/efsPort.py b/service/efsPort.py index 9dd236e7a..11c68bb4a 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -34,18 +34,18 @@ class EfsPort(): @staticmethod def attrDirectMap(values, target, source): for val in values: - target[val] = source.itemModifiedAttributes[val] + target[val] = source.getModifiedItemAttr(val) @staticmethod def getT2MwdSpeed(fit, sFit): fitID = fit.ID propID = None - shipHasMedSlots = fit.ship.itemModifiedAttributes["medSlots"] > 0 - shipPower = fit.ship.itemModifiedAttributes["powerOutput"] + 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.itemModifiedAttributes["rigSize"] + rigSize = fit.ship.getModifiedItemAttr("rigSize") if not shipHasMedSlots: return None @@ -91,7 +91,7 @@ class EfsPort(): sFit.recalc(fit) fit = eos.db.getFit(fitID) sp = fit.maxSpeed - sig = fit.ship.itemModifiedAttributes["signatureRadius"] + sig = fit.ship.getModifiedItemAttr("signatureRadius") propWithBloom.state = oldPropState sFit.recalc(fit) fit = eos.db.getFit(fitID) @@ -99,7 +99,7 @@ class EfsPort(): return { "usingMWD": False, "unpropedSpeed": fit.maxSpeed, - "unpropedSig": fit.ship.itemModifiedAttributes["signatureRadius"] + "unpropedSig": fit.ship.getModifiedItemAttr("signatureRadius") } @staticmethod @@ -117,15 +117,17 @@ class EfsPort(): 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.itemModifiedAttributes["maxRange"] + 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.itemModifiedAttributes["maxRange"] - stats["falloff"] = mod.itemModifiedAttributes["falloffEffectiveness"] + stats["optimal"] = mod.getModifiedItemAttr("maxRange") + stats["falloff"] = mod.getModifiedItemAttr("falloffEffectiveness") EfsPort.attrDirectMap([ "trackingSpeedBonus", "maxRangeBonus", "falloffBonus", "aoeCloudSizeBonus", "aoeVelocityBonus", "missileVelocityBonus", "explosionDelayBonus" @@ -163,21 +165,20 @@ class EfsPort(): ], stats, mod) elif mod.item.group.name == "Burst Jammer": stats["type"] = "Burst Jammer" - mod.itemModifiedAttributes["maxRange"] = mod.itemModifiedAttributes["ecmBurstRange"] + 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" - mod.itemModifiedAttributes["maxRange"] = 0 EfsPort.attrDirectMap(["moduleReactivationDelay"], stats, mod) else: pyfalog.error("Projected module {0} lacks efs export implementation".format(mod.item.name)) - if mod.itemModifiedAttributes["maxRange"] is None: + if mod.getModifiedItemAttr("maxRange", None) is None: pyfalog.error("Projected module {0} has no maxRange".format(mod.item.name)) - stats["optimal"] = mod.itemModifiedAttributes["maxRange"] or 0 - stats["falloff"] = mod.itemModifiedAttributes["falloffEffectiveness"] or 0 + stats["optimal"] = mod.getModifiedItemAttr("maxRange", maxRangeDefault) + stats["falloff"] = mod.getModifiedItemAttr("falloffEffectiveness", falloffDefault) EfsPort.attrDirectMap(["duration", "capacitorNeed"], stats, mod) projections.append(stats) return projections @@ -304,7 +305,7 @@ class EfsPort(): explosionVelocity = 0 aoeFieldRange = 0 if stats.hardpoint == Hardpoint.TURRET: - tracking = stats.itemModifiedAttributes["trackingSpeed"] + tracking = stats.getModifiedItemAttr("trackingSpeed") typeing = "Turret" name = stats.item.name + ", " + stats.charge.name # Bombs share most attributes with missiles despite not needing the hardpoint @@ -317,7 +318,7 @@ class EfsPort(): typeing = "Missile" name = stats.item.name + ", " + stats.charge.name elif stats.hardpoint == Hardpoint.NONE: - aoeFieldRange = stats.itemModifiedAttributes["empFieldRange"] + 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 @@ -428,7 +429,7 @@ class EfsPort(): additionalLaunchers = [] for mod in modSet: clist = list(gamedata_session.query(Item).options(). - filter(Item.groupID == mod.itemModifiedAttributes["chargeGroup1"]).all()) + filter(Item.groupID == mod.getModifiedItemAttr("chargeGroup1")).all()) mods = [mod] charges = [clist[0]] if setType == "launcher": @@ -463,11 +464,11 @@ class EfsPort(): def getCurrentMultipliers(tf): fitMultipliers = {} - getDroneMulti = lambda d: sumDamage(d.itemModifiedAttributes) * d.itemModifiedAttributes["damageMultiplier"] + getDroneMulti = lambda d: sumDamage(d.itemModifiedAttributes) * 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.itemModifiedAttributes["damageMultiplier"] / mod.cycleTime + 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) @@ -482,7 +483,7 @@ class EfsPort(): for weaponTypeSet in [turrets, launchers, drones]: for mod in weaponTypeSet: mod.owner = fit - turrets = list(filter(lambda mod: mod.itemModifiedAttributes["damageMultiplier"], turrets)) + turrets = list(filter(lambda mod: mod.getModifiedItemAttr("damageMultiplier"), turrets)) launchers = list(filter(lambda mod: sumDamage(mod.chargeModifiedAttributes), launchers)) # Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits. From b7900b0b25d2be25268980470f865a0e419d0c19 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Thu, 26 Jul 2018 17:27:03 -0400 Subject: [PATCH 32/49] Change more efsPort syntax to use getters --- service/efsPort.py | 76 +++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/service/efsPort.py b/service/efsPort.py index 11c68bb4a..72c8b1310 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -272,14 +272,14 @@ class EfsPort(): 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"]) + 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"] + 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"], + "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 @@ -310,11 +310,11 @@ class EfsPort(): 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.chargeModifiedAttributes["maxVelocity"] - explosionDelay = stats.chargeModifiedAttributes["explosionDelay"] - damageReductionFactor = stats.chargeModifiedAttributes["aoeDamageReductionFactor"] - explosionRadius = stats.chargeModifiedAttributes["aoeCloudSize"] - explosionVelocity = stats.chargeModifiedAttributes["aoeVelocity"] + 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: @@ -335,33 +335,33 @@ class EfsPort(): weaponSystems.append(statDict) for drone in fit.drones: if drone.dps[0] > 0 and drone.amountActive > 0: - droneAttr = drone.itemModifiedAttributes + droneAttr = drone.getModifiedItemAttr # Drones are using the old tracking formula for trackingSpeed. This updates it to match turrets. - newTracking = droneAttr["trackingSpeed"] / (droneAttr["optimalSigRadius"] / 40000) + 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, + "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.itemModifiedAttributes + fighterAttr = fighter.getModifiedItemAttr abilities = [] - if "fighterAbilityAttackMissileDamageEM" in fighterAttr: + if "fighterAbilityAttackMissileDamageEM" in fighter.item.attributes.keys(): baseRef = "fighterAbilityAttackMissile" ability = EfsPort.getFighterAbilityData(fighterAttr, fighter, baseRef) abilities.append(ability) - if "fighterAbilityMissilesDamageEM" in fighterAttr: + 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"] + "maxSpeed": fighterAttr("maxVelocity"), "abilities": abilities, + "ehp": fighterAttr("shieldCapacity") / 0.8875 * fighter.amountActive, + "volley": fighter.dps[1], "signatureRadius": fighterAttr("signatureRadius") } weaponSystems.append(statDict) return weaponSystems @@ -562,7 +562,7 @@ class EfsPort(): else: fitName = fit.ship.name + ": " + fit.name pyfalog.info("Creating Eve Fleet Simulator data for: " + fit.name) - fitModAttr = fit.ship.itemModifiedAttributes + fitModAttr = fit.ship.getModifiedItemAttr propData = EfsPort.getPropData(fit, sFit) mwdPropSpeed = fit.maxSpeed if includeShipTypeData: @@ -571,9 +571,9 @@ class EfsPort(): moduleNames = EfsPort.getModuleNames(fit) 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 + 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) @@ -583,16 +583,16 @@ class EfsPort(): effectiveTurretSlots *= 9.4 effectiveLauncherSlots *= 15 hullResonance = { - "exp": fitModAttr["explosiveDamageResonance"], "kin": fitModAttr["kineticDamageResonance"], - "therm": fitModAttr["thermalDamageResonance"], "em": fitModAttr["emDamageResonance"] + "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"] + "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"] + "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) @@ -603,17 +603,17 @@ class EfsPort(): "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"], + "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, + "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"], + "droneControlRange": fitModAttr("droneControlRange"), "mass": fitModAttr("mass"), "moduleNames": moduleNames, "projections": projections, "unpropedSpeed": propData["unpropedSpeed"], "unpropedSig": propData["unpropedSig"], "usingMWD": propData["usingMWD"], "mwdPropSpeed": mwdPropSpeed From c1405fa67527c824ec39635eaac5ce569faccdf2 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Thu, 26 Jul 2018 17:39:32 -0400 Subject: [PATCH 33/49] More getter swapping for efsPort --- service/efsPort.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/service/efsPort.py b/service/efsPort.py index 72c8b1310..8d7610ec0 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -458,13 +458,13 @@ class EfsPort(): def sumDamage(attr): totalDamage = 0 for damageType in ["emDamage", "thermalDamage", "kineticDamage", "explosiveDamage"]: - if attr[damageType] is not None: - totalDamage += attr[damageType] + if attr(damageType) is not None: + totalDamage += attr(damageType) return totalDamage def getCurrentMultipliers(tf): fitMultipliers = {} - getDroneMulti = lambda d: sumDamage(d.itemModifiedAttributes) * d.getModifiedItemAttr("damageMultiplier") + 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) @@ -472,7 +472,7 @@ class EfsPort(): fitMultipliers["turrets"] = list(map(getTurretMulti, getFitTurrets(tf))) getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == Hardpoint.MISSILE, f.modules) - getLauncherMulti = lambda mod: sumDamage(mod.chargeModifiedAttributes) / mod.cycleTime + getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.cycleTime fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf))) return fitMultipliers @@ -484,7 +484,7 @@ class EfsPort(): for mod in weaponTypeSet: mod.owner = fit turrets = list(filter(lambda mod: mod.getModifiedItemAttr("damageMultiplier"), turrets)) - launchers = list(filter(lambda mod: sumDamage(mod.chargeModifiedAttributes), launchers)) + 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 From 46e58ecba70ac8160a91e507392751d2e6285db1 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Fri, 27 Jul 2018 06:51:53 -0400 Subject: [PATCH 34/49] Add module typeIDs to data exported by efsPort --- service/efsPort.py | 102 +++++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 45 deletions(-) diff --git a/service/efsPort.py b/service/efsPort.py index 8d7610ec0..0e89d3ee7 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -183,88 +183,98 @@ class EfsPort(): 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 getModuleNames(fit): + def getModuleInfo(fit, padTypeIDs=False): moduleNames = [] - highSlotNames = [] - midSlotNames = [] - lowSlotNames = [] - rigSlotNames = [] - miscSlotNames = [] # subsystems ect + 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: - if mod.slot == 3: - modSlotNames = highSlotNames - elif mod.slot == 2: - modSlotNames = midSlotNames - elif mod.slot == 1: - modSlotNames = lowSlotNames - elif mod.slot == 4: - modSlotNames = rigSlotNames - elif mod.slot == 5: - modSlotNames = miscSlotNames try: if mod.item is not None: if mod.charge is not None: - modSlotNames.append(mod.item.name + ": " + mod.charge.name) + modTypeIDSets[mod.slot].append([mod.item.typeID, mod.charge.typeID]) + moduleNameSets[mod.slot].append(mod.item.name + ": " + mod.charge.name) else: - modSlotNames.append(mod.item.name) + modTypeIDSets[mod.slot].append(mod.item.typeID) + moduleNameSets[mod.slot].append(mod.item.name) else: - modSlotNames.append("Empty Slot") + 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:"], highSlotNames, ["", "Med Slots:"], midSlotNames, - ["", "Low Slots:"], lowSlotNames, ["", "Rig Slots:"], rigSlotNames + ["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]) - if len(miscSlotNames) > 0: - moduleNames.append("") - moduleNames.append("Subsystems:") - moduleNames.extend(miscSlotNames) 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: - moduleNames.append("") - moduleNames.append("Drones:") + modTypeIDs.extend([0, 0]) + modTypeIDs.extend(droneIDs) + moduleNames.extend(["", "Drones:"]) moduleNames.extend(droneNames) if len(fighterNames) > 0: - moduleNames.append("") - moduleNames.append("Fighters:") + modTypeIDs.extend([0, 0]) + modTypeIDs.extend(fighterIDs) + moduleNames.extend(["", "Fighters:"]) moduleNames.extend(fighterNames) if len(fit.implants) > 0: - moduleNames.append("") - moduleNames.append("Implants:") + 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: - moduleNames.append("") - moduleNames.append("Boosters:") + 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: - moduleNames.append("") - moduleNames.append("Command Fits:") + 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: - moduleNames.append("") - moduleNames.append("Projected Modules:") + 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": - moduleNames.append("") - moduleNames.append("Character:") + modTypeIDs.extend([0, 0, 0]) + moduleNames.extend(["", "Character:"]) moduleNames.append(fit.character.name) - - return moduleNames + 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): @@ -568,7 +578,9 @@ class EfsPort(): if includeShipTypeData: mwdPropSpeed = EfsPort.getT2MwdSpeed(fit, sFit) projections = EfsPort.getOutgoingProjectionData(fit) - moduleNames = EfsPort.getModuleNames(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 @@ -579,7 +591,7 @@ class EfsPort(): effectiveLauncherSlots = round(launcherSlots * weaponBonusMultipliers["launcher"], 2) effectiveDroneBandwidth = round(droneBandwidth * weaponBonusMultipliers["droneBandwidth"], 2) # Assume a T2 siege module for dreads - if fit.ship.item.groupID == getGroup("Dreadnought").ID: + if fit.ship.item.group.name == "Dreadnought": effectiveTurretSlots *= 9.4 effectiveLauncherSlots *= 15 hullResonance = { @@ -614,9 +626,9 @@ class EfsPort(): "effectiveLaunchers": effectiveLauncherSlots, "effectiveDroneBandwidth": effectiveDroneBandwidth, "resonance": resonance, "typeID": fit.shipID, "groupID": fit.ship.item.groupID, "shipSize": shipSize, "droneControlRange": fitModAttr("droneControlRange"), "mass": fitModAttr("mass"), - "moduleNames": moduleNames, "projections": projections, "unpropedSpeed": propData["unpropedSpeed"], "unpropedSig": propData["unpropedSig"], - "usingMWD": propData["usingMWD"], "mwdPropSpeed": mwdPropSpeed + "usingMWD": propData["usingMWD"], "mwdPropSpeed": mwdPropSpeed, "projections": projections, + "modTypeIDs": modTypeIDs, "moduleNames": moduleNames } except TypeError: pyfalog.error("Error parsing fit:" + str(fit)) From d9827b445f022f56138a69bf6ef1ec4e2cd98cd4 Mon Sep 17 00:00:00 2001 From: MaruMaruOO Date: Fri, 27 Jul 2018 08:03:21 -0400 Subject: [PATCH 35/49] Add versioning data to EFS export --- service/efsPort.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/service/efsPort.py b/service/efsPort.py index 0e89d3ee7..d1dd394d9 100755 --- a/service/efsPort.py +++ b/service/efsPort.py @@ -8,6 +8,7 @@ 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 @@ -30,6 +31,7 @@ class RigSize(Enum): class EfsPort(): wepTestSet = {} + version = 0.01 @staticmethod def attrDirectMap(values, target, source): @@ -608,7 +610,6 @@ class EfsPort(): } resonance = {"hull": hullResonance, "armor": armorResonance, "shield": shieldResonance} shipSize = EfsPort.getShipSize(fit.ship.item.groupID) - try: dataDict = { "name": fitName, "ehp": fit.ehp, "droneDPS": fit.droneDPS, @@ -628,7 +629,8 @@ class EfsPort(): "droneControlRange": fitModAttr("droneControlRange"), "mass": fitModAttr("mass"), "unpropedSpeed": propData["unpropedSpeed"], "unpropedSig": propData["unpropedSig"], "usingMWD": propData["usingMWD"], "mwdPropSpeed": mwdPropSpeed, "projections": projections, - "modTypeIDs": modTypeIDs, "moduleNames": moduleNames + "modTypeIDs": modTypeIDs, "moduleNames": moduleNames, + "pyfaVersion": pyfaVersion, "efsExportVersion": EfsPort.version } except TypeError: pyfalog.error("Error parsing fit:" + str(fit)) From 083b618b71c36e11fc42af9b7947de76c652c21f Mon Sep 17 00:00:00 2001 From: P Date: Wed, 15 Aug 2018 20:38:05 -0700 Subject: [PATCH 36/49] 1695 fixes + tox test fixes --- service/fit.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/service/fit.py b/service/fit.py index 122de2fa6..51bee531d 100644 --- a/service/fit.py +++ b/service/fit.py @@ -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) @@ -641,13 +640,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 not module.isEmpty and 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 +664,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): """ From 6c12e1c5fc86f3eb50331406daeca041600dc0a5 Mon Sep 17 00:00:00 2001 From: P Date: Wed, 15 Aug 2018 22:23:35 -0700 Subject: [PATCH 37/49] fix bug issue 1690 as well --- service/fit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/service/fit.py b/service/fit.py index 51bee531d..877792d76 100644 --- a/service/fit.py +++ b/service/fit.py @@ -639,14 +639,13 @@ 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 not module.isEmpty and m.slot != module.slot: + 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 From 2001a395e5d53fdd4ea8ae3e2b7978564d15d856 Mon Sep 17 00:00:00 2001 From: P Date: Sun, 19 Aug 2018 17:28:24 -0700 Subject: [PATCH 38/49] extra check, swap instead of clone if unique module --- gui/builtinViews/fittingView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index d6bf4ae0c..b0c993736 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", None) < 1: sFit.cloneModule(self.mainFrame.getActiveFit(), srcIdx, mod2.modPosition) else: sFit.swapModules(self.mainFrame.getActiveFit(), srcIdx, mod2.modPosition) From 2f40365a0369f663ddc595c09de0d0c633ef9e70 Mon Sep 17 00:00:00 2001 From: P Date: Sun, 19 Aug 2018 17:42:55 -0700 Subject: [PATCH 39/49] default return of getModifiedAttr so can use comparison --- gui/builtinViews/fittingView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index b0c993736..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 and mod1.getModifiedItemAttr("maxGroupFitted", None) < 1: + 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) From b4e765abfa828ea4e85cb62166bf1b01854be833 Mon Sep 17 00:00:00 2001 From: P Date: Mon, 20 Aug 2018 21:41:50 -0700 Subject: [PATCH 40/49] add None checks in deleteFit to prevent exceptions --- service/fit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/fit.py b/service/fit.py index 122de2fa6..67c0e1edc 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) From ff222b8d6233c502e856658921492943bb8dd4f9 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 21 Aug 2018 19:39:07 +0300 Subject: [PATCH 41/49] Some fixes to DB conversion script --- scripts/jsonToSql.py | 199 ++++++++++++++++++++----------------------- 1 file changed, 91 insertions(+), 108 deletions(-) diff --git a/scripts/jsonToSql.py b/scripts/jsonToSql.py index 34bab22c0..e4a19e77d 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.append(os.path.realpath(os.path.join(path, ".."))) +sys.path.insert(0, os.path.realpath(os.path.join(path, '..'))) import json import argparse @@ -50,66 +50,66 @@ 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, - "icons": eos.gamedata.Icon, - "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, + 'icons': eos.gamedata.Icon, + '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 @@ -123,23 +123,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 @@ -147,61 +147,44 @@ 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 - def convertTypes(typesData): - """ - Add factionID column to evetypes table. - """ - factionMap = {} - with open(os.path.join(jsonPath, "fsdTypeOverrides.json")) as f: - overridesData = json.load(f) - for typeID, typeData in list(overridesData.items()): - factionID = typeData.get("factionID") - if factionID is not None: - factionMap[int(typeID)] = factionID - for row in typesData: - row['factionID'] = factionMap.get(int(row['typeID'])) - return typesData - data = {} # 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 == "evetypes": - tableData = convertTypes(tableData) - if jsonName == "clonegrades": + if jsonName == 'clonegrades': tableData = convertClones(tableData) data[jsonName] = tableData @@ -209,23 +192,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 @@ -234,31 +217,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']) @@ -274,15 +257,15 @@ 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%'") - print("done") + eos.db.gamedata_engine.execute('UPDATE invtypes SET published = 0 WHERE typeName LIKE '%abyssal%'') + 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) From 6caa79c951ef6f24650bffc242fb12d3bf207dea Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 21 Aug 2018 19:43:41 +0300 Subject: [PATCH 42/49] Merge master into singularity --- scripts/jsonToSql.py | 40 +++++----------------------------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/scripts/jsonToSql.py b/scripts/jsonToSql.py index 7326117b4..a99b9d0f6 100755 --- a/scripts/jsonToSql.py +++ b/scripts/jsonToSql.py @@ -25,11 +25,7 @@ import re # Add eos root path to sys.path so we can import ourselves path = os.path.dirname(__file__) -<<<<<<< HEAD sys.path.insert(0, os.path.realpath(os.path.join(path, '..'))) -======= -sys.path.insert(0, os.path.realpath(os.path.join(path, ".."))) ->>>>>>> master import json import argparse @@ -58,7 +54,6 @@ def main(db, json_path): # Config dict tables = { -<<<<<<< HEAD 'clonegrades': eos.gamedata.AlphaCloneSkill, 'dgmattribs': eos.gamedata.AttributeInfo, 'dgmeffects': eos.gamedata.Effect, @@ -74,22 +69,6 @@ def main(db, json_path): '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, ->>>>>>> master } fieldMapping = { @@ -209,11 +188,7 @@ def main(db, json_path): tableData = convertIcons(tableData) if jsonName == 'phbtraits': tableData = convertTraits(tableData) -<<<<<<< HEAD if jsonName == 'clonegrades': -======= - if jsonName == "clonegrades": ->>>>>>> master tableData = convertClones(tableData) data[jsonName] = tableData @@ -282,7 +257,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() @@ -311,23 +286,18 @@ def main(db, json_path): # 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,)) -<<<<<<< HEAD - eos.db.gamedata_engine.execute('UPDATE invtypes SET published = 0 WHERE typeName LIKE '%abyssal%'') - print('done') -======= - 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") ->>>>>>> master + print('done') if __name__ == '__main__': parser = argparse.ArgumentParser(description='This scripts dumps effects from an sqlite cache dump to mongo') From ccbd0a117bd9b2f9c1a8ea52d76c3c14fa834655 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 21 Aug 2018 19:51:56 +0300 Subject: [PATCH 43/49] Fix merge issue --- scripts/jsonToSql.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/jsonToSql.py b/scripts/jsonToSql.py index a99b9d0f6..2d1b3ed01 100755 --- a/scripts/jsonToSql.py +++ b/scripts/jsonToSql.py @@ -60,7 +60,6 @@ def main(db, json_path): 'dgmtypeattribs': eos.gamedata.Attribute, 'dgmtypeeffects': eos.gamedata.ItemEffect, 'dgmunits': eos.gamedata.Unit, - 'icons': eos.gamedata.Icon, 'evecategories': eos.gamedata.Category, 'evegroups': eos.gamedata.Group, 'invmetagroups': eos.gamedata.MetaGroup, From 21b6a2cdfd9c23db5a57c8ccc2744920fad31323 Mon Sep 17 00:00:00 2001 From: blitzmann Date: Tue, 21 Aug 2018 20:38:29 -0400 Subject: [PATCH 44/49] Update for new effects and ships --- eos/effects/covertopswarpresistance.py | 3 +++ eos/effects/elitebonusmaxdmgmultibonusadd.py | 4 ++++ .../elitebonusreconmaxdmgmultimaxhpt.py | 4 ++++ .../elitebonusreconscanprobestrength2.py | 4 ++++ eve.db | Bin 16203776 -> 16248832 bytes 5 files changed, 15 insertions(+) create mode 100644 eos/effects/covertopswarpresistance.py create mode 100644 eos/effects/elitebonusmaxdmgmultibonusadd.py create mode 100644 eos/effects/elitebonusreconmaxdmgmultimaxhpt.py create mode 100644 eos/effects/elitebonusreconscanprobestrength2.py diff --git a/eos/effects/covertopswarpresistance.py b/eos/effects/covertopswarpresistance.py new file mode 100644 index 000000000..c539e2cc2 --- /dev/null +++ b/eos/effects/covertopswarpresistance.py @@ -0,0 +1,3 @@ +type = "passive" +def handler(fit, src, context): + fit.ship.increaseItemAttr("warpFactor", src.getModifiedItemAttr("eliteBonusCovertOps1"), skill="Covert Ops") diff --git a/eos/effects/elitebonusmaxdmgmultibonusadd.py b/eos/effects/elitebonusmaxdmgmultibonusadd.py new file mode 100644 index 000000000..e291fe8eb --- /dev/null +++ b/eos/effects/elitebonusmaxdmgmultibonusadd.py @@ -0,0 +1,4 @@ +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..71e1923fa --- /dev/null +++ b/eos/effects/elitebonusreconmaxdmgmultimaxhpt.py @@ -0,0 +1,4 @@ +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..75e593e6c --- /dev/null +++ b/eos/effects/elitebonusreconscanprobestrength2.py @@ -0,0 +1,4 @@ +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/eve.db b/eve.db index ecab306284c4d245c97d2f3aa4382b2a8d512dd8..9727bd3fb54a53bfa3f22921a7c4fe47dc7d4343 100644 GIT binary patch literal 16248832 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�aM4NVDh&3Jg`EFLBD)a1yf&LHrKxazmfQ|8DG>V%G7) zs}RME*AX0fDn5h6{TCS+nT!I~L5jBY*Pf< z#@agOcxX=z;p$)z%Z@0!Fej=c#+T0RAsX`0=e@Da^2jJ{Ss6m?r3xqkF%n``urgLG z;P$G7k_TOUlHQVtvRM@{RyLl$ehB|@I5uPAiCZ!LgK~e zkfex%B0dzc&QL@n3;*wnXG}e5(MHA<&Diw+`=S|BPg1mjkrQJ%&RDJgWyzT1jEN^M z*rv!}n^e?nr&&>-oO&#eK58qBR%T5;_cnG*BLiS+3R3#<-$e(0uSxfoYGwr5(Wtd7 zaj4WHno}JDM?%}A2Us0J!|)*rVNiV_>^I@`(g?Oy#zj)-TiCY!jb&J4=VR`SZKs<0 zT@((AwQEG9#j=^F|9GXB^geu>o)uBBCc%QskZ4Y@d`E1e6?g&#)wt-QwJJpzh9ze3 zK4^@r1!B8^!lUXMV=$i{HSD~PnQ+z63vuK7Cp)p}Lq}YUESd)YW zN_?!E0aY^tt$7ibjMETiH#8g98(S$9hl=WmK(w?oKx>U!*klk{OI>+rnqGR@X|N^i z=5+&f7>blhud(cmD-17K@D8sMut?$;VY0@eQq2pEA}kl_5znc1sU|Xy8n$mW&!R%3 zOTKU2lT>s29;_gn`(7Ne?t-?bZTRq<$A*UZM#oZ)`&PHoMi%X|OGPbQ`KTcXiws1` zM7(OQVYA3UuWfYw43Cr2A0%LL@WP;=qcG=z2e_C@?M~jXt+ZznyleJZ?V`0X+kV^+-X> zg?{$EdkMB6J*7Su$<|<(XpLiRnQSx>g|hx(Z9@%Bsr0!h-k@DtA1RJ0ioUyyNb*gU zkdjzQ_RPLXHke`LSHGvh%J#3M`^>cdA0_RVyt_SLd9Lt`bU)_yyWT+Dy}`~S&UM)1 zztl0@{*XP}_MWXF<;xU5?q&;F{$tr<{?2?OZZzA3Gc4CC`SQndo%Au1`5}K>S}?b7 z;+1ZIS0ZQ+m@+92D)$mk>#7whD3)>hOt0I(5-RF@Tlzz5n!~!LFoS5};n^}nd+PDp zIscGwNyrv2EuGj2I({v`x22z+zt|oT5l_Q0W)I`qRNa0`Q~+TPR1->^|JoL>O$u`L zs}XozFEsC(G{jfEpuw1JvXnhGX=N4hwK3&rzhxQn(0fg6*NBXJ)6KTDz<{=%cbuUp zhC2Y0JU=fP4DtgKv%9U2uibbIVO z6oeisTq%?$HE;C`vb$G`bZ9?O#Enifq%)qX9Q%!$SY%RUFG`jwq+(3TVk$=Ei&x(b z8K?#2PvQ$1lMZhl#wq#<%G@p8)sa(e(h63Hd*&~?NT5J)lFk71ruwY6zTXtt1@02o zHN;5`hO{ve<=j}&peG*txVQ8pgCkH`BI=lfX;`S$5Yz|=3n{IOGlM@57{}1WF>e?L zBy=?FKSym-bWKBC>WRl}V%jn$6tcwsh( zOEhs??2f?eD>9kOL_2g9A*{@FC!e)=5|ev;e{_4nSiHnJ?~K6JYbW8<{X}z`Eo=7<=Hl^p0daPV~Y=d=U1P)=M z%M#SfSO&df<%Z$ZTg|pa;K8+zD^`!&Utn{=aE#d`HArCW3TZ#}ozK@sHe)DT9KP-O$wItkuw@m7MlA z!1jA$~vnhZF+aFR`2Ji3>vO|1r`=$$O#a8=TaifHU{Gh@H2< z`48tV=SW1d3p!3fB)US|KWrDfaC9s-;$qouU?AhK)JdvSh$b(Sb5Wor7rr2x1GAoMw1UPQ;_DYvPEoer ztG7h2UKI=su)qbVdfLTwT-9#XGyeKLtCyu!&zNhpdd3{$t7rT|{jIm^?QB_$>ZJ<) z7Y;8}qwFZECSd40!;jyqZrfVmq`V=_tIudY(W)8JkFOavd&nop(CEEYY)8vNRBRwu zjE!aVO%_?r3?CaRrT_H3sl0(^y2taK7T^nE04!8Vvw$|Ff69xZqe+2}F?T2p)xdh3gZmQ3`;NG^Xj2QlO! z)I8pD<*FFECH7aXQppAGwwBq*(j>BMz?#Nin}wA!Rte#7bYsfUaM6caCdp-xzhxE% zDQlbX$&JY)+J@1DV}*JHxxujl&d+;myR|JdH9Iw3hD$qq`YxZL`1-THmws&veAJ8C z-lwG;Ew!FrY@v5+JA(|4|}vqK*Qa)ojC%TRjb9kk&BV&IH)klV_NedWEo+&Kqf<51+-kDXl4_}O z26|b`joJ3b;JaGj`yZJoMrso-V?^V^$Dw9NKsD`ing)@eKGq6OvLJqGh094{18q*M zlNGg$hugSVQ|aorF;Bf7>c&FCM&Hvyes97+ggpe^Rga~b+E7?r8tMVj#y|exia?c456$W3uSMY~I*&A$wz) z(_6?HZ64RRTwI4*i|xv$P`L32Taf$2Iu$KAz~E=egEbTsXx&~M9TJ*pi5!@>kzGM& zrx26_a{SSM>8+kME$|?#;+a`w)02fB{2zZJLC;Bi1v?iViV!Crddn-)zhw{_RoLdm zcX4KPqlMAH#%k)~7U}?{`%4n10GEq8x zr&U#N1|04+KJTpwX#GE0I#=>WJl}cFaDV3B?)t{H&*gJI>D=zLJ31W}M4umPd(^fh z<%^UnQWje8wH8|5v+S@q%-5QWOkbGFln<5jl;QG2azOeP3H_kIy4LaNk0H$Dm^pC* zD%4E2df^p;#ThB8@v<@c0=rE_hZ+g!#BE*CnhLF)*Nx%2rm8AB%N}joQtc5Eg69h7 ztGBQIzTwB-8hLFip*(dx1aaD(NCGoW}{+7G~C%57tQZWxoPB~h5V>+=V z=NFB+_g3tN))D9pp~6;)*`h&OX8iAo z+Lb+Y<1rL8z}mS01#Zu@ zO0o>BY&{-rDGu7y2dXgBvjAKkMdKUK@Dr~?IG{%I;%{y}PTTP?5cKeSA3J7Ss}~uIbsP~S^t;^c*PZ1tlZ*=r4BTf9DDd-c`< zn_CbpMx2zY1RFQ{Yj@-UmH~F37qwrGWqw)TLLLFkLs|yWs%Z(TS?O~Ny=f>yZ-rmm zvPlya6^IChPNsh6 z07X5_iP4;TnEm4O?BV+)xo>ZQ4??->5~k0KhD`NYkXx%;hCTk=TUD_aN&X08%U9Jv zp?VqjA`8WmC+=xaYo6ZUOx~q1ovexOMfO~hXdCpNQ|zegIUT+1y|v0|EvwNe87z7# z#i?Usi>Ol8_()IEAbfgxEqPn)Yk_}*x<%bX_R(hOC5PBH=phBYw7oY#sontP-M6?C zZH*fmpqJN@Hvp~wCrUq;yytq4_uTK`a+%{la>Wb&GY3!RnB_3pckyIT5Z}hUBBHYe+J- z^oF++mOo-`MNq^@5jQxwUgnUy=n8D+@KOv;TRk{q8kn=&3r3ZXRelsvby`WRZ|`m0 zj)Ke)ib}oimc~A3xl!7nZyAbV@J#Do018zE4J+>0X8Zw;rdD$G9MAcO*#@b%LO0W= zA!qHkRWOTo%Nnc+TV~>VpWcc%eyZ_TtaZY|cUZT<#{2d4HFw>TnYaLpTDKt@j8^Xk zv3Ut5j_zM(t%@j1vTCM)Q?R``aB35h=B&E{w|^OIC_Wh?Y0gJ@Fcjow959{NAY zLU4X7d@%*nQ;Fhtj*}H>Q7g2ZMS4r9hq=|=2X2!5%EWz_(FzYkZJP%>PrPgrUnXge zveq(fTTXM(NY*0xeQJ?aiCQE(NvYLA@uaF)odc%v&o87(YlzndD(zj9&Bplp|1=0=!6<467qS@-R z0Jl?La@4;_EICSA;lv~M*J=^`m@GoeR2ZL?*!WMb?yPEs>rQ~Hkt|6{j9dVcCtB8u z;EjSClO@V)e)eHWo@{9=0zQhto-A42`6PL&Yg#ik?~G)r8ht^MC%n2f1JOHbXaHyl zN4b8PvEOu_*WTma?2Y3i?$(*$z4hE_YLGG!jRG+)H8qkqkm8vl>8_F*(zsh{iPW9ChTTIcz--b+~Ay*(C zDLFS_3sh5)xOtNI>#kOW)!Y%WQ^#r{rQ5%iAMPm=dk(B3-!W=LopE8s zY-sMWr}ZDy7ENBOHLY;@DH3NlMdHyq>8UY8>6M6m+*_%4w^GC|aXJobB(>e64#kk0 z+Btm+ZyV3EaX%l+esr*P5>ChQesoV6N9z}tM{F$(J*1bfx4P2$f2wpy@?PTY=Q-O` z<{9RG(%t4>>h9-y#kJQp&iS$P66eXd-~MsOe#dm&PJflX)PADv3B>oGmhx)K)hU6L z3D%FS7g|SJ-nLw9nPGkf_pZ-3k2Kw4nyb8{tWh%MBXSxt_}+gJrHw-EW%FP~h+>1Y zRQxt+d{4yx-fi9#%9}XQlUQOztZmCs3b6+MAD04SJ0>W1l9DKGMgLdrA@;xe(jLRw zpf=8M)0z2-fVdbyXn;JpBeu2zlySxXxX-ywUu(~ZDaUr=RY%GczGk;XaNMs3WX8)PB+TLn#_cCYgMkr;T4WfozV^##W6_HQp9!TXzR&~u%&IbcA^hKrYT-K;^Rcs7;<`G zrqc4*vy9$?u@8}WkV`mu4ux&-QqBm_;39LI7H?UvAi`4gIq>qlw&`Tfu&Z{Ef-=Nq zU^!V?vnNoIpPg9PG_+B4P2mC*HQV%NtTFI>53lr5Sy|hLIWWk*r!J7A$DyJf7-!*~ zp1mabh@IC4=hvuZr=SczQtT(n?UBDEaZ_ZrAuOdh<&9NaLx~||X>pQIXSX5xb<`FW z?So`_&Tbn+AM%Qn^VB*oS&nPlMr)0zt*-mfip70s#LTu4#%91eP(N&$m82Qgw86Pc zc$N3PXZzBztJ@GI+aTh5tSU4$sA9w=+}&AysOyY21lbk~bza*q@e6;_#Eg>PHW)hz zf`J>FqUs-w4p6#BOmc?JY=bMPk>!m1nz#^a+m6>5Kr_7cVdBd#OkSupZ3xp8(~Esb zIJ*xC&u&93?pG_mVU2pT`6T&vZJS4HCvBkgp*qEVsLq@=1QqYCR7u2c zS({5UM`)!=w#QZ6hdr*0Hbi`!qKlJ)sI4kHNr6h+FnyEIx zXiSLm&x`GGTNk=FAY*Yi3xU5>ptV= zicrF1OPwTzRjsFMHZj(5lUY)yXZ699I=gjm&*L$fiF;P-X}GjNXR{K0(n?Z`h}4Sk zoDs3j)IEkfJdEOv%tnPH=2uOjByq+&jJf*FCy8ZAL}@(*We}^)%BBGOX9PkB0SeQL zk&z5p>$Xsu35_{J1B?mL)aS4+Rjqqa2)Ag$?xyHC1GG>y`5_!7;DJ|*Ko>Z))tAj< zTK~_c^?#Fhy>}FD{(IEZ;VJYu+|Rntbx~0Z|w){74{Xj zk8O9_cG@x!6Yv))D^k4H-&oJL=2@P!EHb}mzQR1$^nz)pX^8T;(x&7pR{585m%LqG zCJ&U}JXTx4>~^|gN#`b>ym@ZOP9oYf+7WppI+UXyPR~#B<;?b#xGjNoW{6){0@1@l zX-MdaIIJDpO0*`{0#>ly3689=+wBSrYsbb9?iPXw7U3-TSHaPNiW%*NnyP{Omi)Di z)#9tf+?v~tO_U;T8#>#^hbs{&j_)JDX7(yv5<_Qja1bYPI`iAHYa;fvw&A)7x_GLp zx-lG`ACi5JOPY+IucAmMjeY%mLHiN}`{i1c2eFK7T+t9Fp8;4oRX$Ff{i=4H8y7nQ zqV(bTbwVy~o6^p2Q1!ye)tuGs*bEXnrCzAslg*}bqu9yQ-O!E|_3rpCOHg7m;cFcW zX_mMts@rKv?&ajWojRsVBG9J6+9nca%oYYI7W-@RifnGjmWgnf?4Fap2v*l=-E%Gt z)K(_0OJ4g_oCV>kL> z7&Le;C;q>%%6z~5jJ@rnPzo^u{}-hYDo9CFZCJf0Fp2!gZ}-`x?A5eRjm@4%3=29e zdvr5A`9$|}i_4;REH8z#0!|&*kQh+S9}vUpon(o7du}^cnbFOCZD%M<@y@cBOi%pr zg7)E>C{p*Sir{yGLwK4FljQqAJ5JV!2ptFpzY9+Ad?Sw--dulZF_j2~T-~U92I|dH z=ilRt`>nTTn%|DIHln}Pa+X!&WR+OfP8 zbE}p>ao@gN(2iB!Jf2(C;Cj&&vWUB%xaf1*d0Ayw?O8A?(E%n{{jzb5X z?KrNnS_I*sx4}{d2Bjx4{sMZ(lz027w|*JWjx!sntO7v)sz$uZn##s$?U?gZi>S!r zAyl=7EdCiKK(KM#)u>*rr``IRKDQlj&*EkZLn1}1SuD;hBveFZw1qWgqJbRSw0pV| zw`{l#!Ku|vQK9y*N~v2m)tvbT-uTHw{KoH#6CZuv#arA~j~g9WepX=ZQWZcyHdP3- z@T!2i_m;TeyV~mP(oW$-UJ(c&yb~ljH&e8C#a2*HY3se*d-l}ZLdlN3a;l)M2GI<- zlN$L}gFuBoj%cEdxo|jf9rD_$X|f|8(UMX}^f4z?rOldJLaGQm#)Q_km1~xfW12OS zYYvgLHk|ed;{pm8xWy#}Yxz2ARv0y-ESrl}*`QuRsa|-AuK0g;4@JK=cmtlu1Dd4( z2JE^v9JUbZ1cnKFF7-j3DDI0oF{cgQgHg*xNg2;tJlODkVh&o+1}{OOMU%uRi5`m; zLCuSkSU%>r!A($1DY%s_sC7eBIw#J4ZX1?qV%nw8MNEmlemtiQOEpn%`q8h3^_94v9qVUi$Qf;LHWXH14p^SMr;)GnPF$=Z zZCDKADoBWaF%CHbx(L=DNoiXSQLE~<@N2DVHtJeGB>dm8YHqatpDA@o-lx3#y(_%O z;S4~;V|U-_-iez4zjR&W%6EC4?am^{qmJ?Rzu2#}=i9xu-`MW4HQA0&c_Zb3^^evn z>j=wVEp3*i=HHqRn3tI?rpHYOO`A<)ls_w%E1Q);^7B~z7t0>$SIDTBzr~#u)!UQPn^~TS6 z{3iKyNe66ZVi`e&FB4bxgk$uP>?Jc2=bzs}QPO!Xr9^}+Rc*>+ODU(L8F6noB~`!E z^yp(6n{zsLYvY;J+Kr+*HPRDkww<&?BKT1q0 z#d%4ZsjveYL)Hc=tI9Na`bsCOzCx&ASrv4#G47BODo%ks%b#Mky5FpBSZIJZ3dfQ#I&@NbJ;+ zrnikhiu<)U)jy*H^RHMEiSI;l!(<9cT->c4>v6i61L=t`6BUztw5JmC54{y`W(Ows z@jRZ{qksyP(G&(2; zXP?!r)&>&1xGqY4McmmmF09_jh7qIv(!@duO;W9kiMuRPumlU(MTwNO_V3@yNnLlbOk>$ z59WmeWOiUSN&MyJ4w!RGd6`@(KH?!Dx{A)xs2d)lhjd=#JI`Z&>Wv#0cEI2(%nvb@ z!^hfDsl+85(lL{iD_Sk78Zt(#Y8{%}F5@)%Od^i3s4GIFZF2aAXjwkw}EX zJsevSk%1j_d6C$yRfRWA$hh8~ZfHUqMTI@{m|;k%i{fBCkLwk?q4AOvRQxz9Hac_} zJi`;$En?~LpkOh|N`6yK1&RH{AvpfW1mECSvW7P=zAq zpXz3K#0Q$nYoYP@>zC`-6R?!ghCa8jFiEf&&-2G$juPLvx{yj}IhxfD4RRuvL+#_7 z;`}~{jMeR@XuBoanmE}WG}MXY++WrXJyh_VIo86aWv)cF2nh}$NEsPP!6)dI8 zqg57M70g3AiTQ43JM>dArBWJj6yQ}#GF@vydk?y(_>_y0Y;%2eJFc(fMA-O>H_BwGg=?OU}`qjv>FHU3NHPPV!o z0X0NRu+u{yj4)VqBa1-^_+I7fxUd2 zY{z?)h%nb(?Fo z^Bd>&&T8jm$3GqSI?iw`vVUd2)4sz#+4hO;7l;Wk&!(h2nR07NBqb|FMts0J>sZSl zEEiacEEe;F=0@`*(_5xXP0N(;l%FfL%1HUQ@|p5H>7U5-nErA*F=mB`3}f%GY&r)% z&LS|bOC%!U0Bcw(Y(@7toX$G-h+9aFdP`0ij(>t9az{| z7t2)5+5uM_r)Zk78Uy_eewyM-uFK=< sw}th!?s;e)hE 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�&Bx2uj<6<5BevMcc8Kq}Bw@uP}gsW?_qUjEVYk@8#0zE-wS*5v=W|9AbT z{CD_1=bQ4~gzkTXB|j*68QuSWTKvx91H}bJpD2nJ-Q)ef_gB16c()Y(Lt(NoSn%V5 zUn@9P;Pw2jXS68=5lMPTJQja)8onj=TzV6CL7PrP=Jj&dL)uahU82c1_JSX)KJ3G& zldh?Bf%K&yYKD!Sbw(o{vfKLPjQbp0QxG|niOP1!4K<{atp`&OI2m!Tjwtfp#;gmb zDJtK|Xip)#`IvpV*DV z+M2QnBt9a`FcGv>2VpwAea>sP^@thSa zH5(#RQ=?HvdUt-TNkQ=JHa^e44bxdDHyw&u)TAJ9a*tLhUdL0{%hE~nPBT1IjEW)# z1@%Dum%K8Wk4g~{tVtoQCimPF@wNS_N@;7F{Ak~Cht6cftIt{$HWKw_ zL0dn)Vi8i^90ymC0+Vrtqbfr37Zo)KrB(^nxQaX&^G@J%GF$-`ohqo1>P@Rifgy#= zkbO=#&8pQkuFlE{naAD`@O@lY=Cf2B^|sZgq+;iQ!+m0O_fbxU0;U7e83=>TWje|^w{tH`M#u5tivoS}=TJ2HYtXOv#o zw$*V-e1jWo^Acu?)Ro*1dE@GsWC;UJ5G^m9j#cFQWTxvGtR)cft^#}*!%>ZhR`2R* zmwga;vUo}#^a(YnZP^+#?Oz>{nzh5`%9R@>Xu5NKc=ePN?GCWE4L1x*ZR#FfMG4E3 z8G4MdTQ=}~$#HZQLy7JrHF1HLDHw<8PSxzepAUnq%{R2P4)_^IOiivGIjt=^w| z-|4L?oGEx2wtrc6(Eq>VpR}hyT4opfJyR^}U@GTJ%q`G%(7?}K{xzqN*T4+e3^?fl z<=3GW((~P^$Niq!w>@ZLQEmbGY9%1FRcVi!@lf#rbnhHFITD?$9*aCRGB!O{JvAMV zN2jW1qLGQ%cy&B7J{+x%j1N^$i2z;oB=gKvH(edG_ zQ`NDP{J(}o4%(LLh8X^ow}Zt4_jn>L9<81*RIciQ$YgYA%kwv+9xd}cf7A0fnTsM{ z1}%yO>-eLX`fH@wy;6`A@-Uc*Aa~7*r$g!NS!j()J!1dtwJkp1QEyEh$0ig!Utq4+ z%oNF&^4Bu*Q0if_1SGjLL2`1m`{CZn~jI1yVPt|xVbE(bY7 zE8I1%x+itmj%j7yYs73%9kMUmikUwZA5Hbk-e{A9@exF*c_5F!Rv(SC+*|n4W24h7 z&CtxERP)3hLerR4Qz~RIt^&=S zaIL8ZBTixhbwOj`i8VB-5Hv20Nrh8E!&l>qrA$yOcJrA!>!;lp z7D^$nii#Kcz7Iwxreg7JM=TjCTrj$$>`3i30vSM$Y)Hsa=2$Tnls- zR!s_t>r}w4o@LhPPzq^SL}BxdL5CT;ld3I+ge^L%Cmm>$^;LJYU<&C~)K`a<>JUlu z#Arw%hl+a5Td0yJ$nF$UrXHkoB^ak~c6@QJd`}M;I$8cy4Fv-pq0R zr>0MzRy|QumqJ35*^Uc$0Tfe|Kd^Kl_QVaOr8~{TDI_gXm|upvr@_6mqPmEmG7Xu1 zAayqiqxiZ7j8W{Q3-QJjlAd<*qhT*Bp}+~V|2Lx{f7RB?$13j(d>vi?{1v}j5vd52 z|9<)7<)vl6Rd%$j(EscHc69&$4c{ZaKM7^8T&& zZQh!~pA`ObVSB+(5%UkC(|_(h|G69hlWVWVNxqw}*i;W*K_Cp7chE8wPLRE8=oTQt zy8QQfs1A{}>oH?W*ddLRiBl1!lR+vYWZf5?!e1d%AvV)G-pEtH`Ve7Luz%+oBAn#) zp-)I%6U9Lj(-w)ySxSSi`qrwX!^;^^e!%o3XrT72A(ToEFG95`_Yf8q-CK92J!^=s z=ESrc47!-%wF-GSpMaW->xboPL73#1Bp+Tw5S7ljD~v*&J-UWo0GWPGroJ2V%B{;Q z_&U6Xm?_QBJWMud_C$cPV!mgsRPI4^hSP}W_m1{WPfWz4JV(-DbKM2x@wF0}5@3V} z8fTo6J012|8I?c2hLG_L-4`Bto7%-7S@ECm(aA@?$BHIt@0#o9zWlU=d zab-&7avDyO(fEQpty)uv16#2iNMp{{9jh~is4HCyG%q`rMMKb>Tzd)uQ;`~$+b=`w z)o9n&o;u?QXWwmYxVF@^)QSy}A@8a7k@3@jHJ99>>Y&)2nvz}wM>L6S-hfKTqp8Vk zN!}hC2J4{rWRE<}lK&hA=6bc$$PeF{-?@@$G(WPjTImf4p=aC|4% zr83q*lG~jOov9(eXBx3fi&RXg$i-wT!d(fj{51E&JFR(z@Ag^F-RarvjppD(XN=l}PWJyCYM|4aU}{yn~b z@Xh;rd=;giFMXjjTv}N2d3669F1fk*3&m%PTZ(-}pF#J(J>DO9-|ZbN{H4Mz1z#(e zFF0Iqm*?;BsaN{Xy=%}?I=DxJT>>I$PCjdDIZ>f)4QseI;kNSoJwwxoaU-ZCtx)Fh zW?}C>K)4@(ht_bFod|Fa=3#RZvNDehPRNmo(b;|;R}$O2bq)6vUan(IuQ_CA*BY)M zgj(_#qi8ACwT8PVKSV{Ub=Mj$o%|3L*TeIVtv!JlwD)Z&*Oj4A#d%^LW9C6QNeT3M zoUHpZ^BW>ac{oW}|L<}g8rE=IHMaUQC7@V@WEpoR46fmN3aLQcFCaS)nv8irO_KOV#jO1}<;%$M9(2)GUow zs6(}D$lh@o->e*ubRC!R9m?^KyN*|>G>vPI$tYhASBW@KpU6n%MUNGD%@*BbI8D7eX@ z^Pw0DGnik3h4EZf#Y1aIg*Z)nhu`P!oFUK$`8QTUw%%z|Ga?B2Uzg zwJw>K%uuu2apaBHX~oNKM3N_7c&$Uae#ti!MvI21v7@?aB`s_1wr-S>rx1xg9UUTV zhw3Sin4`lOp*5tEP-v8i07DToxl<3N)r!KXVrC3+KGi{KTx*fiR@gMzR*??Qfi+}( zP<&3{NQtODm4>xOdFY}uoSpIXYr}LLwPaR*miyV+zlPk7z5MJ5kbYD!S=?c&;YjAN z4`=&h?u`5J+BKwbko*&0@14x&^F3=w+YsjbC~{Ib?Tn=6tV@TI(G}NM!y0lpNS2X) zl9l8@Ld{hqwX7lh+CyYDCMFv4;ToN29X}as85zPm_Mtw7o*P_47&auN8&7;QOP~2n zG^W$Ec9*={MSP86@Z32nlF|hVuiYty0|7bIMrq8aaqSLyio|^Ek8Z8CvX32-*gBK5bgNS<-I=JBP}BmS%`=okWgu2cqM+s!rtU zHm0ULWjtX=hDADv2C47@>tK(r-Qf3hJ%7Dv4k2c2WnLutPfbq}6#iO@Q@Jhhy}+*r zo`Vx71ZYMD*CM%H1Lwdx z^dH+)AW3DJpYUcWG@*Ofp$Xl$m3a;)n36-T4L+CY@xfC_^b2~VdCc{PqAW+0&}?0Y8UaTJrs6)VvBM}9>R5-)aGalJ z#TZ$;m0V4xP1jxC7O!Ln9HIyU8!Q{NJz+ zJ%nz%!HMav=_zD|U@Q^`>EE>uQO$ajoWcZqYR7=l8ogs3D#(3A3EKkP*S%vMss|0T zf7E^0wsoi+ck{1V7wkT0`+6M)u|jg+e#>&XlPbF zGL9M;WaezCTd#$7)8@w!z=L>0-7yqX%Au4)r2sg(zTfY;{rQ{ucc%yXW|6oy)`7O! z)dNv{265@pQPkQ1HijCU>d2YM$Y^9>G+I45Juu0CT+OD-98{6&e!PuP-P+n(&2;E3 z``2rv;ufLD0Z2k=ip{<2$Oomor>D80CVULYJ8eHR>Q2mk>&OD7umFvPLd5+FI)fis z-z_t2O)rH-v6|!=U&6C11WRmON>Qb<)v~@z>TRGZt36_QuB=g{gM#9|%tzOe4*D!( zG#@3vqY-D{R5UWmtm3f&)GUldhoL+m!agblb>>S|?jLAbN5&}Wm~2b={&h5MbuRC8 zhiY0!+9(Ml1FG4867r0+XBE)4ETE79WGmcs2MVrlHMCy2I7A`k;>L6))3Ux<5=bx_ z%gKhlHa^K;@?uY=P4SkZIF;_x39TcIluS^9GEgQt0OX~{4d1+epLEx7<~2JPH9m#r zltbVNHbV`hmi2q>OEcy}he_gTK((&lBlAi!mL}Y6T;mX6^O4)cKgQ5K4K#Rnt# zw#aW-hlN1*5+Pj+NN~McFp-a(T1{vWg_laXk<3Zcs+0(Q>$1m_&zc2mJa}prV^}nK zC2G_9ZQ`8x1`+;=Au<7XV@qswNVZFn(^FWhee1UhA5~t{Q=o>|(dns`Uy>&EnWF>3 zP|8vb)(Wnp!xNdVSunaTf+OqKgHos2B{~p8WdlpL6kQ?kUsRaX9UX^QoeiC0VL38u zAjWW~RP!O#k+InHI4;duT!SbN5j{K}T4%YjOhfh2uem)OM4wYu@fsJp3-*IOi*Ts( zP+wzs-+C3hy4YmSL`P$2eo8S0Ej=f^Udf2c21i)6($uWUF{BP?!Q8(dK<5Tb+s~WY zz@m+e;m#+&8g8lv%@T}sAuS{_=(2y}F8Z3+%jKD#G)rT66-Z|FkHVADuJyvy{Ls*!*C*!fPYGxEet=qC|4f%WIk`^B$G9#1UooI*F&}T)sq>ZKW zJbu1dLoo?QM%Bm^SUJE29e6M7o+R`%`-t|9+V_>?d`svsr9 zx3{Z_pKN7Fh1vf*JeNFGAE>%q)l*ef`T5E#m0gv01b!TNe_%N9hKkQuJdgVSZB>HAAhl-^eI*^+mZ>?r`pPmg|KI9BE^k2S|P-Ju{5+4DbL?K47!R%k= zzK>lSxP}n62DjNUgRJrCwIFtG;7&Th5PhzQ+b~5T*Hlx)1?4owtYzyWc5mQvia0E~ znb{jqi_LZGLiTLnn&Nwtv1+(V;7p5HNU6Q>&JA2rPLy=O6ais(N_KAGx^e={UiUUF zz>bYugg+x=)mdAkhu5|NHzfB!WZ-3RqaNC^4YVYjblBz$G)tp0E?&uF>`uz#8}M_^ zIACY3{94c-*?65a0_|rgd!w!m{zhl`Y{t44_@0d$oaIzDyu|_NPVerG*E%2BEP880 z_H4Yyd79Zo*2e7IfHKEVn7cUD25i`X4kv;&T;r29*Q`6Bt`DqdgK2d$Ovrb3heRcb zsblst-_8u|vg&wrA~F)MJ{cc@u{JtejbGz0;QOIwC-~2MCaZ^8V0p{_4QOc;7>mR4 z!cx5Qn-a9Pj44^N9<-qlft?hNk!-L`>E;^8Yu+d|l;0dsT_;bX;z8M7qICmhOH{t$Kz*@7O{{|( zC{oHvElGQAFw`XF1d}pJ6Rdp$Dn8{xJFHTIHI5IXI6KNbr|vu1z5!)FC!AcUHeA~V z41=5}KU0L?vI&7Al`zaS`cE)!FYg>QTg<{4A zXrrNoDJT66?$AU#xDFdD=Rze~II49#53a+o$_XYHstMM*4)aQN)-|!3*U{LLTxt#{ zigTGJQr-HD^aPNpWRKQ5C2C+)EUU4*)I zBy!eroq0^=nsYCiF2HT;k3#3*1ZSCL=z~lx?maDRi&CgPZ(M>-*jY5m zANyz;SK^bo%|267v6!Ri&?8T~yDD&a12^QD19XN6yar(0mrlTL`$&Fg)r@g>Iss=9 z+QJ|@z=t+)jSA;`VLeLZ!+}9oVZ9L#MUIp#bv2S1o2cWpfEJlU*~f_UaCky+)m%B;y|4BWZVhP`rF zrBI`dGm$ZF`vy{41|7yZ3M)Cyhc{|ve`SX~H1&U$x-2huOH#VKH0s>g@9f;`l?YdcYYgo7?t<%Qe3@g`^5N?bGcsvpJf-MGscR1)D@Kq>!! z!&CKZRqd7kRr$Wk$1AT3d?YX$xUJ$ZD^eAYR@_|v)$&w%e|c5eXUoo(HI@1NpZBNy z$NjhZ{?2#RcNF>mpT!FRyG#D1`k}G1Xap}?LxBOXe=_MalmWS$cYwJnLLYD zj%AM@M_FpWJ8#sdkq%9Dq4sa1K1=Ft?g$6cDD9%^uuKG9h+rBjXoxOg+ahG54Ow=s zXR;w3GNNmCj0k%(MdmWQuVrt#!FeaR&g?#RS2`&3Z-@@a?w!&}{j5(Puy27(jbS~M z#QVhVpFWVTw{L+JLH@ftL`}L*rp43jDYV!)I5@_uG~Gvs(y#`aEgNW#&O^7WNki@5 z?TErFPu=IRFAW`kr{id*b-NGlNbi%?9MrC|j; zV1eQFtvs{AeLcI=Q1TzN479wqJ4fWf4 z`e~?0z!pj83XD)1y0(=83B#<`q11H+%KkJ|YYK!D7YLLwy-5VqW*#^fK{Q;Xr0ZdX z($JZ+7h+~#inbx{n;1?*VYafL+(gmkDHhj~D~)L=%WalT&4Q7C;69y}G!$hKG311y za{%&8L{TxH&%tU*Lr=~P#m`i4bZ8nmhOu$?Ssh42iM3u_q*cpz#B>~b`_s@^tqg-( z(^k~(K7()?%BgjmFx)0D^={z(X{e(av#^!wyU(H~4YkuMb}ZaDzc0Grd(yz0iPxMm_m3~ndkD|IJBL;5xc5Fv1NLBi=VDX&6OC$d)Bh|s`Uda!wJ&km*V-7Qu23&q{ zy=i2EMHyU%yHXmN1{@KNvyW=U9cd(lQSq+bE06+NoP8d+gZG!zbo zj5VvKcQB32u#AKeGN~MbWh$`4X(Wd^1+lnSFJor;1gVdvktwDVkhO#gjg#trTA2O6 z%ky4O)u*c7UKNA~;EyZcR#_YPcVz!R5qM+87kCdqPx;5n-(Fr{_VcojlueYq+5flx zbN=1v0r)Gv_R@bVeW|pwqFx_>{MM#$Exq7n0veQRy& z$#Z97o{$Za4cor&_yLD!%}^9^wrulBCl3zh-v7K52mN>rW#EERsfZ$ z>dYaOp0bmIsF3GO+R_LHi#v-<3N$`P<;;-K=xpV?U89PU*@kpnssTjUj_AOTusiXR>{&p^O#jn8i}S(7kcJK=YiRk zM((Lo380-F^FTb9e$3ErovwnsPHZTRyj1H&jXqP>dqwZiwxmyFc&Y3tc?h-o^dokc zK`(1fIF%Q|aVLa41Y$6aWG1VJmgbNbVfUqv*^Oj@p(O-s(?{)kOWzpEJN0E?Fnz?n z06spy>%BI8*kKO(C$u$vNLuUz@z`n9;SjU(ymYahx=xSQbboHFJpDLr={~7RWzWml zV%?dA(@0+_!K1ZbuYGOp-kQ(Vq-%nK#Xy(;yZ$%(AMkzFH|q0yH@yAT|5E+V>L$-$d1gI# zxj*Ngb??e@$r|@_32+fl7)L@)!74pbv7LFRNhQhYJ!i(&3$5UB&YDyLOsBOOZNEW^t zv#lb;JjKdF1t`WN?b>e!vT)_tmmxd**(_M^lCxR(cg$o+L9rs(Q;K1IBv}#GnEhEe zfUJr>7%@q`NEswNLspRbiS6u{L!QmTU1T+KBczRy4`$&&8aFv}Vm6JKY>6a(@4+mb zO7>Be`r6i0dN~UR(X%GBa_vDO)gWTGPmqTZ$P3-p_>W}aIWjv7MtrT(Z5MnvdpoN2 zx@=HyV&$>>k^U^aPju56d>h6QG>Ug$r1sL0><-ZF*&f}tzw&4nPOIy7Kf*01w$U$U zZ?SX)C>y7jY!r@e&xLz2`wDx|+C5_r`ibn#=CSX_#WdM4_P!9$qG+;sgjeVEGk(G_ z;}r zU!D(n9`U%{?{`1xu62FLm2&NIe#QB#&QR5ltCp)SqSpUM@B#ek$--e{h^8qi52R)+ zHCgk~-YmQ~hDZ<m|P;NZF`3nwlb+i*Qucx}p#Rlq6Qm}IOyS-5M;qKUEEMeEMO zPh$vrQD=@HDZ5~uS@>tp>aQ~uOz*mojIt{WPYsXUnn|Pd*LZRyoSsc)5@|7EDzc>Z zZC4f!8gnc$UAD10vvADtAsXo7lVuyKJqwRajLnt-icXbnptD(cW4H>flqNMGib;c5 zZ7bd4Y!<#538t7lyO{l1IBB@`pd6ii6753vW#P5q?*DR-bTaJ%4rbxRF}oMV1S*WX zlDpv1EZjO~nxOuA(cT401P^)9#+v(`%);+8Ym%GJCsSj-RH)aenU81T+IUWXYPErl zX5HEKt<;ybrmrOnZ^v~L`ox@?eL+LuXf_6^!fQgI6@aRF*(CFcEUfphG5Lzv=VoMT zbWtp@)LJHrXh2`JOIwz({;o}fA>?zaT*-mjiXdh=l<{f$a0I5Eh z<>7>gftT>~wrq#uB2o^k5u-yA>eiNRmxIKlAW=C;SPBx6gUHH1cV)xkZn81K*5~Y8 zwoQaY4#zVImOdiZMx^!)B%Yp4(mH(^^FK3Z~XlDI4nG!=fn5YG3nFMqxi#$aulZu9TZAtyKEh8n88O?69(iztj zbcu#zf+blW^7C0FJeoz&m%`IDF5AG5XAjwxcXVlHwm)z_+hmt=ng(YZ_^~W94b5iU z-&*&_b;XEzQyy8p5O2sc7NUdI(NkFbba2HbRBg$ zoWF&r|F2f1t8Q_8kh=3m&?H`vn)QGgQJmFTr(`RV8+T$qPiag@9aOA+kvzu6ZOWtTW5dMaPC&&TQGcy z^ETx}#vN6GkE$^T|Dg7{M&{&35C(q`5&{=$yY?fD1^+TWHHmj1T61vxJxhmTS`U_g zo=0@?qM|y4lR5bJm@aCHpm`h8to!-g%LVH+?QZd9>qC4#2ag@QO-zCL$joT62ELes ztB%>|O@Wo@m&&2f=3XLfFy_#)$}u0x!SzRGCh`!+weirSGV!3SE(-tM;o&fnL(OSAp?YqtvsOF zlvo3H<=|IiMoKdZqe!#S98_zp%Q-mGtk}Iq>T6Oo1d%F6Up26lDlg~YOf$n59Pb6< zKbC_>%}mZI75%nD{v$cK)yzU-jN<%uz`JwsBpD3q)S4!4D~;PgTJP*!_Bo-^>lF_% z5lUh8$^^;F*;kn_U&j*X>s3yf>U^Q|X&b9En^ycoLZCNbsluH#7w^s@Q_{dm zR46`3yI?(8pA8xY^0gL_?KJa5w2xNMjf`*ZNJn~mS>@KTcT zY@zj(K9qw)n<+l}RX2g8h@JhXa`0Erne4LiU3jcN-)RFmc&DvW1vET}e^VYXKbR}y zp)OCAjU6|XgNNEG60ww2J9!Q9T(nF;7~OV?L$ZmLH(-!MXeN|xr`))el!;@<9`-Y{-TWITu!*}4C4?tpu$P`Y@x7*9r7>d;J!1{ z)&#DRdaiEUEq8G_2PYoIH_$C+$4IvZ-wyqc<>2kJ&<0;1`H$q_`=i>RfqXq<!k|q<|dg=9e4xYI~w3jwT*H+*D9K3F3 zH=>v%jXFWSZqw2jpTQiQZ8R>)uuDrJ38JFuSzn5SIr!MjqMi7bG06++IF<^Gj_zm}hc(EcCVQAdcpCmyr;~geo%73vuH< zmfK}637LPTQZFvi{~H}CNBuJ5|NpJ-x9d&@|1tO$`YkpYsuA1J!e+8BT zeg6OQXZ>BipZgYlkNRr7pY*=Y+gkmd>gDQg&yPI6?m6lHXZLTokGX#AdcSMj)!_V^ z^KH&p)z7O|tNI-O<#<0nLH>m1;cgAHds&174yDr5iP4k@PK>onY$>66_+G6sI%F6_ z^S5d)mq98b=q&b%j0U4rdNP^0f*WBh4tI>T!aWcFD^<7&Gt|I}ypby^UZml+L)LTHJ`b-g3z1KZOlrHb1xk(1P9&muu6ablo7(2# zrIk?=mTb1jJp8d;Y{LrME~SG=2yL^&!xM?F$;sI%$;RlKhYOZ@`Z8kQqg)vimzg*o zok-3kaG9m1B&iLj=HZ)d(Vrm(#A_yp!`3q#nunj(aDa5R((rGC!ci`GXh3D2DD)-{rLG7Q1`&K!V8I_3`XyrWF4rF}Z zk4vS>WWVV#U&xIzG!IX%nP9E|M5Q;$rFl4T&EkRdu$1yjv0LZiy=|ql0-iwmNQKN` zXx<}iU7R)X%p%q40B1YQpM+yk>~Q!H`FHE>8k%<_e}L)wq9e_{XA3knYO+w`qGMkf zICOFz$&EafLts?Uy?q|eXT~oHYY|gR3IG_tWIdMHe3jxtQGv7rs0^=t-eE!*#j6Zx zDEAt%c6u{zi3oIvrG3DdIC53`z)w%^)xzSSz^c2U6x(|ChH`L!(^F*-TE*3`NP`s- zIh2F<+ZJq7C1W4T!R>7ZTQcklVd3<)g4KvA<5TgO*>nOy3`tpqtTQ?Iy%kBt$Y{5t z2M^@n>ZVqDgVrj>za7w?930+dJ*1`kzTgxQr+UEiL(hiiif0$%|9{JU*}cd0udd&7y~8!@dc<|m`Jc{DIA7=Nb~;i0 zf1+xq<6HPx#ea^^WBkl8Kb9CxAU7O2Xqm*!a0=BGC4A-RJc11l(v?Z#Ri$(?J|TKa zbiddBRKx@C0%U`3hMd^<(qMiEpGGOBdI=>*4vweU5#!>j*PA z*!$#>c?1S9T8(?c`h)k)UxZ9c<^4(aa2k~u@rGP@A~icYh!T!etPf30+&bTlZ)lrF zVjc9y#+~y>+C6Oc(@N|ADM68ZpY|$JHkuPrnxdw z!}D-ochGfJ%q5}rMs6vzTSP8A4=1$&CA90PUa>we02%&dGi0IkY#aHJdH9rRJOxpHH8fiz+Y{Oz znTKoH4BUD`?15jJhnHEAVKtOnE~7o{3-fR_o6Wby&a;QyJr92~*WrZ8Nac^*;F_AU z3kE;40Ze6+6@$USYyeZaWrbi}^YASX>aT&0c76*~h}Ad$pb`Npri_~0!zom@XyTw1 z*p&O{;i;z9Ux#u>l>%}quO;kQf4k=4q&AG#1Ttm^t8X4|X&&)fe!LY}yD!Y&XTFrLD+C^zhl|?ArBWem$2`2$XY?Ch-R|Nz zvmI+ar~UK0OLnK$lq>FFlK6FItR02Tr7oYYd3d4?yOTn01FL`jZb4o`@q1AzK}%#s z7K5(&20@+~WJOp{%-B{Cn{I!P(#u zQ~~%%?X$HH*LhHlO&{rMs{GsQXXTSSD+(p;~s$E6bd6&zXcb={KS=DD9-@zxh_vhh-bFkb}C{w1IzBa?c(7p%53r=h#v0%> zZR-Z4t0BKG&ZYeR@Lj0#&M;h1Hx&Q%AorC_$=b&}rE(t3&?ynx`Sqm)k!Q*kj9nH;B+KxLGS^2~+2%7HhnEIN6oLC&!te!aUwoPmwJ%?N{ECjMLN^M7<6ArS}ZE>T^4wtAkJ zN7%z*+EeFOwAK08{Dh!b;~})w^~n5H;q_7}rX!V{tE2NziJ@v{$zIQ-=&7OBPEVr$ zH={s5^#6C(U$5U)_vdx5t2-S0cJOzCJ+=Q?`@Y%^)Bsqmxl(gq;G2Q>2F^kA|CIl= z{v*Es>-&%|*`dsy0o`3NCj;F)(Qum*^U+wO5H@Lp% z+H}3eHRd|z{Je9*8L0Yf)h|^wBP-w|+cN>MUMCj2#PVin=q=-_!Xag2%ny6N7liF=SC(Z;pL%4`1A}B-3((eK5-3p z0vi}lEW&U18hTuiwAD2=I-5bA;P?c+AlQ|JOJ5rydq&%=iCj$9BYk2K?!4`Pp;7*Y z@x_Pyj=j$}`XD>?^@myRH_8wUT}fsduH$NLn9acM0uxL+F`1f4G~l;}>sOK^R~j-n zs|nHwCK%FSYL?jfh&5(9k(!=JG$fhrW|+qOAK14DMRbswp~wajPxgkUMsdC3pU_S4 z&!H=mko-dJdl#W}c38Hi?e1EH!=t%OJKEy-B2-XTV@=t(G#xk^gS`pg#y|0G{F5qM z>hp^bRT>web+YP!R(*AS_AL^ZghEjpW!U5$Lh(Ia>-~$+AulMcnK2IYK$B90oemL- z6s9TM*0XpY!rfm&J5Zx7{h@6F;kyt+usAfhZEBQ-&P|Q#LU4eq3sz|iFWyUn=eLI@ zbd|=YMQDk%IEh0_lG5qS;$D1uhvC!ImJKr_nQdm*)(3=R1Z9hRb}a4zBFe2mbX4gm zMa*R4czDVR>G&cvLv_oT7}%pR^f%eJ;l@LYPz^^Y5$Yh=C%N%k-q~ z##;d$%BR~GZ^00}<+`%L>jz;zhSo*o!?CKRCSxd%qYmQuBGTX3v9O7t_U%|iYS|e@ zb)kz+62b;Y((y^8gr$Wl(;+hzoL)qJSxAq>f^fNl8XA&8gcgxoX1M$rgz#m^F?ts3 zMQEaOHQ*g>@shj%qIx?tGl|aJzE~&Bn`UTYa^yfQi!h`#8)ip+&1R@wqAGPP!jxib zon%95S%lfeK#3-hSDJ9zvIxUW`TkPvE3ydNjX~HXJ1&yUq-Upb{Heeyg*mqX&69r<3Dw0&B!PeIC@ISo^Pz=GD9J|SuJn0t*}WGpSD zFEKllj!z)en=xIsF=ZFX*(Lb0MwIhh0)^co|b+nuVf^zSkaV|e+eA>Kg7(J+(3zfDiXu@erwYza=R-BcpGLiDcmFbS&Y zSt9Dki=>$yX<32_a_?nAXo~u1`B!F$UXK3}g`54*kf53UG$;c0huM)C60{@cbS_VkCU-{yH?Ra<$t=^9JLMo7@N&vToZ}wXRN7iN zh%LhZ@TkH}7`ITBPF$Z;kd;QcB9)2AnA6Thh=up>AZ9b}$mU&9{9`yi^ZHjWKI3b)B3&cBC^6%uLG|`J~FoAJqLIqG8JX&Tp?W zzw%41^jAo*iHXsu$yfO=ILIx$KA8eaT zoD8cb(MhXcVXPw2V`iU^PC_9}Bjs*#*m?}s&G;y+8}V_djk*>`MQ|tEMcUabX>EOY zx)vdC@;FxNIM8C1kY|n6vpB3s!X;SBs;c9#MvE*$e&tGDDVk>ftzkMAA+d6;w*S>X+b69Q}?$uNbBNbif~v0!jFM9!l}ha zm9Uu-1a{V~0iuf+l@JLEz}SQ`8+Nx%coE714_eXSRCp}Hi_jrTQ7SwTrxp>O%VUW& zv}l=!^`!JI4vHWxcG3-(8K;L$Ra-iBRhEzT^x|2+nf*k2OCJ3>-@M;nIc8X#LIXXaEa^~Rxh@l%Tk zHa`0NUhOx@Mc*BtWM5Tp{f4LFlQ6}MH;iitLD27HY8LW)xsP%%`myJoi)Y9PFKNHV zEOm5*T~j0J1j2;bwG2cI0rEiu%4dtPWEOIa*?~09++5>inht2kNfGG>8%7hDED#OI zE2--Zld~gN4m^M7VxQk}_?PN+RM2ae4H@WcjB!d8328n8WS~K~X z0}ZIy%m$#DdUp33=AI}w3V~F!h|{?|5M7MvkDH!>!4+Z8cN|^p)#K<{s%@N(#U2B5 z6+#GSgSL^5Eq04w1%5lmCRuhe@vm5#Oj!Q^n;rEZuYYCz(Yk-Edr#fDy4v6$1~b8Z zwco6L2mJp(ty!#jyk>vkhk;_?@j$)*WBy0|xA^|d_shN$-k*5i>pkNQR)41Y+3H64 z{eRap=<&Kg?7oUB0N-*gxJF#NoPXndr}M0{w(4_Ludh1b_&dj&%?$#BOE7qiQ<)Yb z=L}xCVNbu8HB7dJAI1id7$$p`nBfa$wXmzExf3VDd90cuM>jSQt!Ha+35KuAXtYs8 zs%;6CPmtznpi#JxIBAmLk<&{E&w(QWAj2cn-^zCpvTR8>7DcfhUE30(bQm3ah)NP$ z!b^zP;nxTz9E^%bCuvP?Z8D!m>k?vjRMU-$f?y*)9}>N*eF*_PP%QbRmbECh$-=Ep zEg_sojgDoW6@L8?Z@6?UElW>nP5%sln(OV2pDP zOriD&h%6YbHbF=m= zJz_v(CN&lXR4H25(j_6Da@y1_#}REGqv(<%)|rj70vw2SJ&Yl;`O=NhHcrnH1Uucl z8^>13`R-VPfX7q1j;|7==+ap+Q`nv)-YJcJ3F#ujlc*F6h4l_?T{}MpNrwJZ8n)ChYSrLBNX;60;QCqm7QNW42^w00 z$im!T84?++K{`7dr!r^>A`R8DhHYDd1jFho&~l2f6^Td|Eu1w-_Y%Yxmgx-;U1Uz> zU~Nl~U=)gUwQTHD)Vb89m?cLhH72^s=?N`Gg-<#WRjXSwbz*S1UTX5Y=#|1Me2S-V?1=g{2L|hC6ti)BO z%1$9kpndn!;S#c7RhgL3-ZV^>LokX&qKqz~$O*fTRkHZ&jT(}qQ{T0OdM4cdC>fp< zt9uFMOVn~3B~!)B2x6Z}2H3uYdL_&kQ!>DcfTBw%Pf}*4v_LYqL}Up;w%i`7%v9-& z%V7GK5PcihdrLICfaOU44~7>??tY zUhJ;~g6RJ(j=v)M|7+`;>wZ-C{<_IJU+|N`8R-9CtNnGn0q~2Ot(vKt-GRRjyeaUo z|3CbB|C9c^eShzJkMBv}L*8F_Kk9wXJLs*h{%rNzs)wr&R=>>i@18Gve%teer`7#a z_quz+-RSxeDgwS8^#FIcs+=Ejrk(d!{e9JYtDdfU0H1#G{|qd{Y@J}bA2mV_QiBp> zx@-fozd3{Sx;T-`Y{+7fWmuiD8KDKK%wL3UGB|q9B;h0vEJLb4>mZ&q95R@>m@qIo zJwfXT@~4jIUnbJ0mm#_vq65XJsKqkY))==I5~9lk%2^~LBzu9T4Hm(x^d&S!-ICDV zz6=?gGlvo!A!pl}far2RC5Y&w)*S%#3kpYE5bRAwMCM&4~^o6nk}oy(m_Y@rm5%_b1Np;>XrPp6hq z0)P<)8+3e%si*7`L%A7bz)MVi&C5vkKdP`p%DdRnKv!uX^IPVOAa${uf02Gdm`8(RD6E;6>bl0>FQz>Rm$dn?THy zmXwYxL(t{BXC$4Ro++nVS+CU{%cn7ybgkl}aj1CL5_CRMiw}kJ66qt$5H^)f5Sp2R zP%XBCrCqjp88RPVByvh(-4Rg{gx%(rypl2 zGclOrA*zb^&5)--oC7u zGDMo)YWruHE5ou+%Q7Sv7F|2Z4c|26+^*Wwy$peciBY45jv5G3wT0dm)_daKWk@YO zbdKTUNRG|1^DEw|A{Cc7mv3H%V1h*^tS~Xd(nx25HJF5Z^4Ld z0W>Z{0(qi@P_}{bx*+6;6f%iM68*SqxJJ5v86w98^FC(nPb?#fk0oI;*92o!?W}x; zB=`pvt-e!CztnVQ+>hj1lM98m+-vy zWkjy2GBxc(tx9ZDbS7t}Q^~2BjFj(2ml5Ws%G4?lTQ1u;r!PCyaOrC!P+Y#x@Cqvo%`RGU^#{9V;SMrm0>@FVBwMoO8S zXx%@$gj^MslWILtyjmNbb*W>pZRxtOSz|7yDGqhcu|3a0B+TW+(lzCN)PZ1QN+~$K zG)oUKS$cHVCwVBuL4KDz01fL4tQTyfEx72pk{H*2! zHL02hYFvS@2R;^fHgMGc4gWj+m;6rOvTxXTr}wMg--7;MTm8Rj48XTNdCz0+pS$1X zzU1EF`hx4Nu0huW&VO*e-Felyuj*f`Hmc54z0~m+_+{s+_JH0zrZWpF=Y(JXchwtMFMrQWuk3-f(^Z=b_>*2Pn+@@V^ zT`-EB{-;)U2r~tlbt()fRt7r&kFOwb{yF_Nh+bzU9R?FqnN%8lcA6;JWF3g^0mqqe zvD*8LIrCf$o)82(41T@z@2SUZr-xnpbYt?TgL|_r=S_z96JvPr&lX zy)dCd~9sZ?9g(01-gf-VHx9n>_CK8peHcJmn;`OJVTw8jV81LMZtj4qj4|TqXQa( z0V8^`QaYd_7%?hbnM*5B3o=Gtzzinbc!-l^R)ZkniP=#Yfbk8I|0lJ~ z%!x%!qlz4Tjwv)TF&BoVh^s;PH|rbb)H397Zeb^KJPZZ`!k&_U$1-H+qqvdbk)g3L zVgv9kv1b|Lag-V|XW?j1j7H5`9p1A%W24!_ZOe!xMTiQ7Z3{F>lxEv<+6IV3XJ}h~ z+6IWk*&AD)R-`O4`vVD9m71B8xcOR`6X~wEUd^`U6eS8Ql&~x=wL$cqNN^51A$O}r zm#2hkX6<8>IK3jvlY*10I6R&KcLOiiM1m14EePGq6XF6lg`qFK@@qm07Fxbaj|^AV zv=hWHV)^_WT1F`82({}Evy=sP({UdnNg3!*K!2FwHfMSiGmkDKfb=@uxsqP|@H0@w zIchygL(5kv|HQZ?=oP^pSRO}!3iCp!5_BT1+mZ~Cjq?2R7%ivC=2tjY4IQ>Kz%$DU zS{v08xT3K=xE#m6Vyc9tf0~W#((;q^_9Fz8X>L9)+TD8fi2i@tk#p4l5&Zw*dRN`2 z>Yk}<4E|GaK6o+sa4=B&x3%-NkJsLfcL3j9(-QbW;7v90{rc)l)%SaT?)h`i`#ev3`aJiz|JnWf?l-v~aX;w#JJ;J> z5$E?&CE!<_oyZCJeAP8n3HT-mDEH^YDkN7`Lg2rkmmnEGfIvcvI%xQ_bQ*eA z;cl>tCV#YL6`F(^Y(@vf2CUJ@nF&N%t7_jWG>SoGfvm7deYRET4xGt}(^2we@GTTH#>CmzY8DHfClya@< zOJ>&Fy8>a}V7ek!2blMU6T$?93X2UZFbTKl2SH6uLNe`W-?svx)!rlmaNi03w$vMe|!ZJB(tg+;a?p3A6bF0Ijfyq#iAWDCuj#>;gdrp^W=yv+9g}&u@%Utpk3XH9NZ@DlBwCTGEAd( zKzjz~lY!DB8EfCW0(nzqU;JiOOl{m8+1_;L3IxsL`ZG(}7i-bce)kFjD|z^kU;?y( zCV1eXl}m{Eryz(9V0KeTPO9lt6HU_3&g2~{7nvf@Xy^0^lXJLz_X=Vr!wOyecb#Dl zY+={Rd59oND|-HhBHEig)ZWy+a!#mpWSUr`D7Q_ z-j%_UhFG9XlGWuVByr>c}{z>|;h#7d0H-`KX3$XiN1lbi{sp@qbmuiskE z4XyMFGm%0BBLU1mK7tf_A}?EGoLK46DgboF&ZHBasfkf}=lfQWS7f*3 zy(^uVF#Yyn3IlT%@hmVr+I#m(yZI?VkPQpU`hqLvkeN*aVatzS}BX|ZJmsW68A zTH#RE|HKM{EmaMbZqShMR?9lKtQ;%(xR!XixBF;Ge=YH{{>=aXMo0a3>R0QZtiPr1 zi*<#%H`Fx;|2w!E>oqy_lmGj}MA66Br zu2kLa_^RWLdWHYYDl@Y1>r0!@B(AF%0*(8Vh}TcBps=*{wur2bij7KmkDBvZDa?o# z2L4#iGSmyrjw!x%71y(x&<+@FBB33G=qfI8HEab4La9&#qU_+buHur{P|~>#N+1Pj zDCyiLCB3VUYi~TQH5Kj_asAWtWM^RQt4PpLLoHcJ{_Oy@t)es#Ed6{hGWL)7QxY6) zo0`+B5Q9{M$c4mk#RKYFg;;c1p@Y<&jNtN!4<}K4k_95GDz3d5f*hsJP~VwBMRBeS zsC5;hmSztruOd}2J&?oH=`n`Y+8NEQtB}4_dr@bsGZZ6l(V!wi@9J5xv*IM(^4#=z z8V`bINc1^3kSi(Sj9**&0nV=Wiv&y^9JQ}98QTQdnmz4WgjYP-a{^y4nHtfbAtx`;t}0v=c7ZwF+s62@f+oK2ALe3_#_r8S6nu zR-=NMmcybEbP|~8YD6%gaxl#6F2NaEg^*^;9;jv3QhQ+6DkL`5=Ej`2C25Q8$!B2r zl4TKfuZFZLh7OBaa_IiG2iCs|*)7gC0ga$Rz6~O1GL)CFm>OZsVh0&HEs|UI;_4}% z<7xYlx;g>Yo27rXr6OwCBO#Ut`TQ!R&Pn^!YHA2r)7rDzOzNvL?SW2h7XqyiKMhYD z{ZHMzYBXR7y!r-EkES16eOQ<|G6pc>s@1#?r|(M_Z7?x0W=-tL)h3bBFiHw;b9s|< zGx>{Ls}BiQqU~;jkG8KOd{MoBXb;NHn4SgoX_$C5)6!VH)2sWnw}%GEHpFeK`;f;* zU8xyXGlWvY7+UWtvR05CPdsjAo#WhoX=B`x)%yk8Q`F$ngcfO)*2wBTB``XMKt)n1 zOlWm)iBisBh=gGeqjhzURseukj%uZJE}+s9PnArqU)`UBF$j4T-Ak)?Q9mu08|h>Shaswk zk7@;6#j?Y0@`$YXz`*LAyo|6KUZKS{vx!MTn^%=c)yUuwJFn7hK9WboWEbUFCf`qw!0$=t zUvgo$1qxcjp7pn+r3v1P|`f6=}oVHbz`1kO!hSg^+n;_C-_v$mUZ=u!QtFzWr(>CdHZJ?op zAhg-Pif}V+$&(N%1jFk5DA8UTliq<g*-4$SUt|HKkMG#~#Yg)SK zR;mE1fAxxDX%OWkc ztlwAny}J3jOLZ>^{z-5#_?lpA?JsJxi2lE&=1YkFZwve)uoXxK8vI}O7yWPY_xtO8 zU-bQ|FXa8Hx9Gj(4OD-j`q!%?o?m$0?@4;vJUiWAa6jul<@%}X!>(sthn+um{+=`G zytnEfq5og4x(OeB5&uN<*GiN)-Mz@}GHI>Vi+L~&=Fd=5ew4S7b%^vvXXZQRiIG?W z4(4ZY%qgdh6Q?9h|9U*t2g{>+B6-BnvMYjYan;SHbw?2e%=(%sk5uG!^!hfH*M~0d?jPq@*rB15&(O6+wma z2v1Tg1#l?Mn~gTcbuy30B$X|)4ZtJzNO#F5IF&!Ay&5WK>PPZJwBRS-MZ)+h=jXfg z2>-TZHsnUxlOK>V8`7*JkN77W%!ag;F`ItvK2guMp4rHOqWLp6n2j8#GvBA(Zz@4X zW+R6R=VRJCkdBC+CC#n{ie$?~^N4X*xhO>`JxFq#j(m@H%_xr9Ig%rt&Ub6&J%*&I zxX~zwh~&Gpk|&+z(uf7vQLT4RYraz}t1*9`GAN?&%lr|S@*UgtsY|g>=i9aOq>50& z*U~5*U`rkWZEEbe3c+`yYL`QE57d!I(3%-9Gah)%f%R@YnLkx}Hz;cjgS&P2<~-uj zRC%3UEkqN;V_0MXtb51u2t9jLFJ5S3OboAFW~6@QNWQsbxixq^W-RtW12EP?71QWp>wO_1#ZEdL5S@WryH`GLGJb}*yt_2SGzwf`{zv92o_s_l+ z-wf*i{gt=iecZdF`Y%xbKZaKTf9UxG&#dRL`zP-AyPtCJc74tD8?IiL$N72W{C7J& zRiCJuuG(GYg#Q0)1_Qvc0>(5+MQOu}IW*+$d@?)l|WHq#*;{aWyHTW9W5Yo z;Ocg|-dRA*084ti!m8m>zY+Zy{YG~I9(;pNL6tS;MrZZc#Wo7HS#N`$0$lp4ZH|6G zvurH0fh#MSSes-^Mhd0Qeco9EDurn)z`3smysKwko9fUZ8QG}r88g%tP9C7 zItve)i#G_@nH`y#MdDj>rk|FKmW?V}*k{hk6%K3f3-Hye8i%?+ zjC@=+uGYeRTA`wJze^(|S_=1SZ*9gaxfTVXL>eT~Qn;tIztH4t^lvS|wXT}5O2r~6 z1;+~TqN{o?N0_uj{K*15<*J6nr%%K>N@J3C6yO%e>i`A?hscyoZ3Q^ERX)O@2jbI7 z7W;@ANpcmer2v1iDpGL}s)^g`A1S~)tja;!{@kW-3#P4byUAE68Dg%;kgd6WR{=4L zY9@dQE%veQD7ZqnWjoC3i!>|vZL)S2I~B5+<7HhroPGzU6LUx3nrvZ=ptOYoj=Nhg(H(Ero!|;$U*w;z-FkS@4_fF0r_> z+DE4fV1}yXQ@Xok0L=xjRgx|@ zI`Xds*oX>7=bQO!Rj@em<2Iz}W1i)ZI`eZ{iM~{?pa+(XPr`ot^VhZhf7(&+LRQ}v zdj_*;Cs?4Q$XP1>DHhbxwW4^PJ2 z&aO3<`WQnoaq6NI$r)xypl1@#j$wODODs8O*Py2w{g$!b7lhfm28}`$Q0P!QW4+W= z%8r(-cwg(9PNxtE8W~51aB>Ygg(_nS1aS??x`)=FSE$|_(OoCv$S_W=LH1`iDMfigp@(z|O4tO14e5sb(&TGo&o z#6&@*72TGJQCnY$u0fFJ4g(UQOde+g%bQAsQ>bZ^x{g}*qfqsw(T`1Q5J}@y6H`M~ zkkLCC$3t$0yN|Qy!KUMD5S3DrPI^&H>AQCgcQq>`W&S~=-?xVco9djE=+Rz@dvPF>sLa4Zr#rTjJ z<%OO76i0Hpx(f)T;-+;Y3|JWAWf2QS>x5A>>-`!oJSCQ)qvDoR_7TdiiS>|vdrx5mI*(!}q~Go@9l@Q2Va$`hneH>qg}8Q~QXK6> zS}((~0z#ZrJE?vKth*jBAgW1C++=)E+m`0~_bZ;rJ*u+{Igmq8HClU7e8L_? za{=)gD)%;6jN}A#nh>Cq)MXIug{aofqMJvpp#?El)(djF5Yf`YSF$3A&H_RWcnBIZ zcSDm$f$M>T{_py; z{vo{k_gUYYeSN-{dcW*_m-n)_q56NSKUO_keZKl=^{t+tdA{oTsOR0D=RA*lT44eB zn)`~F_l=36uANIM0F&YP(|}hizNlR@aCg~KomiXI zhO=wt#YX32YcnOH#sF+Mwz_7@b(OKt<7?>>NrQj2N!GV(4M`pB*i9#g@RGBzI|#oX zWq*EX=Ne?&az8g7-?;|) zwd~KwcCJB0E&KD)ookRn%l>?1=NiP&vOgc*x%Q~V&ksk|5XH_oE3=*R<HbEga_tyE4x2W`6*2G*dTP=l`lvjf4y za8)L@L+H>NG?gUTwq-z7LMw5g46Z?M88?9q$0wjtCAWwA*cw!sIeNzQOJzjW95(Jt zVP{UZXi#&gN6s)89DY0Vdv*=6$Yz)!aO3p$rs0t_1SvC%x_%n;nA@9#p|#`Y8$t#y zo7-c;+o^cFVnPz?jtua3Bk5%s!Be1f^axsGhSv~LZIJZ`RcAdD17b4Y?xi(^QfJtv z5;jsq#m%JWsEbFqPNIr;LF?m{*k3QNA+Y+{ve??V=xyPL*B&mJ0p^_=fFn?lE{o?n zy>>_=0BLdz=>*U!%`yn)|9^+0{-gDo`upqtzV4HCudaKz?q$Ir1iuh`Gu{9^TKl8g z&(^-9_DNI$_)5*YYx-(l68ML}rvmA~9{=b4&-i!4`~R%(0iVPB74PqOU*m1@x~e~o z8i03s{>t+~&!p!e_fOrQcK@=w+x0)LCFB7(oJHqly!`jEs;8^&ga7{>rSAXEbxgiN zJq?ZF(Tw5QGKb0m+b6kH70oRQfr!G>j-XO?o(m}W%?2cc+x~?Pgj7~w~oMu%lb{PlEw~b zXC;7V))9`+L@R|r%?aHGV&6I<^$jFy5Si<#5)!-C_X{1^jKqr8mb76g-cx}v#$QOXzxsH=-*p22#O=?Xzv3@TqXXv%}=yl^8-K@NDGLxR2CJ_$o zvS@ESvA%cPotqlhcVnB$zAXG0>}i3+>vu!Wp?wn9soW`;uKJ$18LcPQ@7gY*;l}p$ zI|LIUE|#frjl#=jf>aiwSE=>M-?_e%>DPFaN!-P%RpLm;I%3zE$x-}F&oSpz!h>n! zrarTdNOE?9M@gx0BpA?M1Bs_+X{Bab)0pxb#nog@djI;(B2d8`kwq$00(o-%Wm=yO zB~X1}1tq1dDLT6TQc=&GoGv~|h&Pe;Y+Xm(F!P2nI|ikio9ihquVf= z$w1^q^`~@va(E(f@&;_QEm2|&BgMr~T5Ob5cJGfTp)jN=^cnv54a^z^on$JJ$u!X4 z_TGS9mc{C5yR)nTSg?>KK-=AW196T$lmKlzaTqi$`);bNKcE7sY&KN zb|b01zO2e#*}4wT-MAti)m7DQA*$+vRT#sha=-w>%p_Kvfg7+R8sl{=aH9zKkb(~0 zfPqb7b+SytsM;#z^+gsk$%7^986WW9*jovfp{*J`1`!P+jrH4qPjArs(|euMAS{n@!N2H*g=S zW7A9>iR%yHZNnS&T)glZDp&TO)alXrIBsIicR4o7czm1tX? zlEjzXuFtgNMzxTslwG40s_eT=qc=R36xsEdw%u@x4P#1?E$=kZ|BpL<(^3DS`f2$8 z|E%s+b^C&U7ySQ%Cu+Z2`_9_FT2IXK%S=$?|_rZ``KXHE2KwOPV0hlh|cohc>X!?`3xug-?&w5;^IL(_R+lsg0Kd=4EvLjAT%4iMiSg1kzswwcV*V&ZHLOx4Nbv z)RMVUpfqE(M(x`G8)I^Y5jC8eo~s=2!Uou0%5+L%aWS({>MBRxxe;V6%;!i8#@RMI zarH_fJ{s;ncXR_>@*w^2b)q5A+uv4!XHn+lv>^l@C}i*x&~EnMQb3KhJH zfk?&?5GDq-Ngb3mjiC)h7ctqvfCeGb<~UDpAg+idJmPsBAuKGfN4ZZm{SjVky*DE3lOn3YFpG-JPDLWgG&ZkKh!7@G;5{5q zkEg^cSQ9a@ju;xItSW=hN;)L?g}P~U3YKJK(RHZ)YKb#C3aza|#N5crvz%Io#@|92 z1|d#M7gHu=5lg4mq2iYSFo7vPiQ7{KaB>~Gyc(9zDS+%|@B7W`(Aw4TGBE&NhCuf5 zUF*=JX@s>;S)0C8W(-LIGqc2)gEF*<$U4+x78^r%5uI!ZsYrz)_uyc1oGYr<`yjLq zrC7tL3@~t!74bhzU$=_@W{P{d)DptwRUeIRaefV{RQn2ep!5E5g4<%i8sBG4orF|U=x52q(>Lru8WZTyfJz)Sc z^^(be&a5M3!r(d57>3!zSLB@OTZf6jV0O|Va`iyR6|E<&YaNjiYk(at;mB~6njW=w>)jV!NAQCI$MmF0aESgNb-dS6{}IIh zAF2CE-G|}-KNkFVM9{#W=u?aTU}^F8Wo^8VQSL2uf7p!)mO3)PpZ>ph?J%y{lW4#4}}6Yh5RUU#+Y zTdvQzR$Ot{PFMs!=zJS$0lcK@3svh?Z>&mGwK@J3q-g$h7hfXs181PPCP$DTBVKm` zU!_rAROzJk0VP~+sCbijc`BWlOwEW~wp3c9-1n!hC(`5<9(G~U;t~fiab$@1h6k+}0hGJd0CRj7tWLFU?pjrr2i$b>tZFE-=iXcnpDEU;3wrDLv;bR^t zDm@_Wi#u6k{X?TvM3T(XkzzobS;Yy7kYQNQL$nBI7~7G?Pb(IAdhtmJxVs1~lsz_M zz#&g)Q-niz7h%*hZKFJ4d#JStgPm~B@rQBfUQzj|B*3t(2>q4?kSko7TcT1K5dtI& z1`QXX)pqDPiE7TWi*kDrIwMO8oP?uScyNg^B$B*#TM_DDBNa}_uRL3j|9JPtYs6g9 zAGv~ zXjY_24qwN{tgv#IFkCuTmD@@N*|C9im@{;PU zx;EAasCh%NFsQv7?e@NB0~Q8W-6O5MIM9Pc8bsz%FA^2p@VL!g8?YL%+sfFGgh-%8 z!^W_<7mY0h$lV)H2<~NU!oP<4%Nrd+S)extokE<^Yjau`pw5#kncou|?WIm&5fdQ` zsqEQ+Qm3&vnvasK3;*RzYG!Ua5!rx9t)^3Q!9p-)gxjF#sf{XMts4-0 z*|kpCohghpk>L#`_Ez04+MNR5Hpr1|k>-s<+I3+SSkaBGi!A2o;6_tL?&0>P`!^8z zcm{X8-$9)KJWPnDE5Y`BAvtpeb<0>J(B6&x;)W#N6I4P&GJ`rCac$q3C?b_HUL{k;b&}{Db2E5mYn3zL7XEYQ_92x5cIlOTn4F7m5Q$I9z zqnD;+wIhv>j_TOPy-M07^VY?Yq7V(o+ESCl)}IV*+#?t{bM(dH<5LMd8#{@ABu8vv z9NXBd|9r3Q&!5`ZBQ7HL`Hpn_8eDR=zkPTEA&>hs(@o@FGVF}?xZ5{&DFzxkWfu~| zG+9oFYU;9E1BEvbKgq>f3dB~6ho{*^IkwTDpF&hEr`7)kldqIi`Z~UGmtn(E?IubW zUos1)Hty6`2EY|?8vv&_b}B|Yc3c?s&rDCGNOF-4i15bk`W0YW z1yPccsL!ax8C$O-(f^M*KIEu>fBjhf&2^uto2hFI{zvf7f}6n$wf|H5f!gb}hiiUV z^OrThS2I%cvcRVUuM4#J|I@$WANTL|{iE-_z6Ioj-LhI)|LDst;HFT2-UttN7px{Bu|FA{Q=vU`m*?jIfyUkxhMt_B$AT2XnAf>rQ-IbA_AH+?Bo$~n~FR_coWaoZWH_dA`I4q$7>RxsR4`M zauAu=PDO4^M=>U(Vlq5c$S`SZy#@M;oh5cXDn04zVrf5a4Hzpz08;Z3I6=4@%T0^q zsy$nT2sBb=%FAI&B>DS_u$r@K+GP3+0bDu*YgZArXlCdw(Pa47RoPjh6?GLMTG_>t z456#27@*~JNv1efgaKMzGF8DXH@%W^Mv5>yYlJ-*1o$=(f&*95WjhHbXO%2B?)gPb9)OP&p{=Or zpB`KaPt}4!dTDm{cgWm;;UZCF6wYse;T|F>%BdpM9A@@WwwOwc4KiAb(128(lairx z^+`#H6dx#=4-JCdkwlBovJ74y4UFx1jTE7A8J340l}eWf3YcMeR2aL1I#q-&W>_8- z#m4gV7NLl-I5cH_RRGQ*6?s^?R!;cjCWi^xm}Bzq)+C-U zLOJ99Pv zo*uTG1Br~z)zebERYbXwM%vsLTO!%%A|jjB?L&P*{X@dgV+*3M_zJBQrk0)+F5OCX zvRDy0h3bg~Am0ewMO=a>k%s$yuRvxsa~#rs`_m8EuO#h-0=LeC+2yn`%CWM za$j`6!u4g>@4B9Lop#;g{J!&#o$qy?b-JrQST$C4xT?zWCG%GR4sSxB<2ky<_%nK# zbU{_&xseGP*WUENCgeAs4P@-6918JNa^ofhG@hU&YaES5FZOixH6GZ67piU_c=4*xnMTPTV**J^rm5%r-`&LvTG9}8i~U2J4rbA_#82aqS!L*h-vahXKHNB z```C&LJBj7WBY?J5q_Ef3V|3lUcqkJ#2w#EyAH=^W+oCN>Dgo^k!IqZJbSQp zQ@P-E{e>wfJ~b}6r*ZZGDf7QPL|L(Kyp5Gid<&(Td>SZpNIaa2r1xht}3 zdXq?jvhMkc%{n~6NqHmh83O?3@Azg=80<%;XI1<3P>Oh`tU*JYHDX9aXc1>$n0#$X z9k*{Hut^P}A%3M4!cBPJ9h-<=I!MXpViArn|LvYl7-X5pn{%Ornq*A(BX8KSiLfLV z3PwN}r_hUEjhl!{V%H790**NHq8M0-u+sG4ribzvP{I4amRh!X<3pQnNH26Kq`4}+ zl42;c+u0)GtC+dn8dMny zsV8rZIaEX}R+4f?3rv`=i2I9YsMsL2z4(l>UtYvc z%#LOtx?r_Af~EmvG&W14q}=}9gv0_@OG-!p+KdRfpHsCK7 z5kdAe-N%;1ZZgF1kwL{QHHPn8}*;5e|!C8eGA?I`~j>0kJsHC{7mrq;9xLV z`}NwD+Q)16*Zf^gu_jrwJMfLbI|F9}4+LEPuls-B{}%rv{w9CG_buN?U=^6~MSXXA z|Hk`vZyziIpRRsQb+hL`J%8?5^gQbs@Z9flxWD4wbidL4r2ClrCfDD&KIHlh*A>_W zZgu{^`8nsj^I60R>~~gIZB<>Z+6%&7*q`tgtX&2RbxHJfC@itKgtuVxlA}mWE#WQ5 z_;M79em$@SC4hMx5i*ia%gmJ++#sYt>y5o<3*x@vu^v86sSH=2L+yw5Y{Bv*ZPv7B z>m@dtHSXEEseCgY7*9Wn{EBiPRhr?W&^x5f8uxBN3t<;M5svAZRX2n>)V*7^cG@-V z-GXK!$HRA_eUxpd4CBz=E$AWgcK9wXe0jUZ{aa8;L{2{*Iz~%b`1G*vLiLch!*`)`$lEpEvjt^C z)$Ppp1Nrbho6t0DG;F$O6N-h6hK=`ZLaQj-P&4LxHlZrmXoyck9k9``v27EQUAfE_ zaR_83QA3*$d$ALkhJz}F-V|eot zriW=r2Ckq^BgSC8@xq%I?Vw1`KzQ?l9TbVg72Z5=fuc;16y=;96v>DVYz|P*6NCg* zgu*MqsO1zh*PrP=d$@b^EDAZ*69Eo?KCl%1_#GRlXHlOfj7s##Be}?t2LoP?zx7o=oi+-mv zOR9)z^ht#_+r{Oj)P4#_NDNh`ze9PE$@;X!HlvDrQ7Oa0AH7iXr7uL3yiUpKYu^kD z1r@d(@}9JUVw4xH32WbMvk4^G_wAb@n?RCdxO4L~36AkobJ<8bMe4Hkr_wV>EGO03 z_HCX*=pX$#V|X@0lP3uG-$Z;Qk>0=ALWFAqmMnQDT)$)JN!q>HjJ2li<0K6iJ9vyd zX-@vVb6TJt+&qERWHe~r7=gHs2R%ikImHt_Zr(%`>b-Q^j;8SN)@Xcga3Y?$(y)1$ zg$gN7QBiG%A9nA56a7Et_&CY`pQ%4k_uaY$X#P8bUkd(uFdl3U-dg(uRR4cZZK^g@ z`;wZ^)I3{rAn@(L?*`5Us{CK@FZfgbu>V%y4}JgJ_b%VlzOe6R?>~7z>V1nh?mg}e zRDYv-v-*wIk5)hE`Gx0;o}A~Z=Zt5c`(NF^=YGcBf!ct7;rbO<#QFa_|Hk=I=ewO# z&UWWrRX?rzQq^YF8>*hHYN@)-@jZ}M{?CJ3PYK6@n#t9h8c&j|mdzKYh;lN_TjRd1 zBw1Wy&n#^&d`e2|hOH~YnS&Qf86+usqbf^H4OL>wwinn5U%G($4N-Yp>$k;+EcrW9!!=Sl&i)bHDZOvF|oMc)!m z;^5)iJdMKe{heEoI9MQ?!Wsu>;<%0IopJjXBn{@!qf>!*yAm_7q>)+$?T2=4L4r^{ zsw_V*8l$nh2)d<>n!;P90s>NygcXf-%19r>-hzO@qA3+RyHT-GWLu_1a+Ni+jBG(d zFa%}lVCbk(!_^w*=+=;M-73@|pN(C!mQLtnTZ6*$t?h}ULpAB=!hP9YbcGPFU;rmvSK=rKtC9ofK@^(4G@xXbq``F?B<0zS_Fh_iRDB zG1#a$Eiqlg2eu&87$#03m!BAZawCoRZ5`t`s5(MUtFh_st)oN;(@jP+W^8re){zoG zJ{)7y`?nxvX^&inXfh-hSnxY~+UP$Tw;m=&0rhy2wk-FUryiH~xqs_WDL&0LE^T{o zt4Wa^sS}alYm$pgd-QBIicmsj^^|F53!^SeEF!sly0#v|UZBibLF3c54J-sX8nF19 zR*V(dI#6=AMq?GjbZ@D_jpY@s?)Hw%bSgC=nX8U11UJ&mCyVpTNR%iwU0Vo#q%&oO z#Pt~o*8Z&r#HElCH#s9h*%i9|uy5;ryQd$!XW!O+Wm=bT7PP)+>s}kY0uArox~C+l zQXyBISBQnM-fitIVcP~qLEANK?Gd8AG7T!1piOpf?Ji*?bg%0^xNmD0@}pYliBF`j z;X&lViPQ{xKPQoCh&LcCll*N%27i5^ZR>8x+xqiU;)s(8Ds5X0Hb5l1vu*1x8z7R+ z)V6hJc@SZul?^AnbqBYc>B^?^?yA~jEupq;?X*FPWIW-m+c_!fHKUq#6jCJSmGIVW zc2Fb>6yDll2SqX>qW{Mof8wbBTK(_WzokA}e@ET->hg83tvgeTHn{^woRIsbV<^5^KH}G?rqcbrOnpX>HD5DGrtA+ z0i>M#X@3%t1ZK|cXU_SaW!_Z(mHJ=63xMtQzQEV<9>Dtok-(wAo&In5zu^BB|6AY- z(CTmUeG`blZ}_sl$9>0q_jteS{hap$-nV%l_qKcep09g8>-iHtTYOqr%+>{kycMisvGV#P%v;)t%bN#(J*jLJ8^^FDFg>1rQ zn{#uu+8;2rf3;To1BUj2eN=CMz}S9EP6IwPW@hMXZ--#;g0Cm{=4NYA?ln^0l>-7& zCIuTPcXvXgqs(Uea)3d~ggIlm4ukNH4ge0RKD2IQ2Rd`WF(!pHXGUi*CZUHK+EKAK zojKqT>#n6dU^{ZKrw|AYF?#K`WTe-D98e1Bjk|WGZgT19%mKQPy<;KHgt3c1Dh0V+ zIl?lycW^$APA>_@Br!()&JN}PGZ1LyvrWqsXcJKgGfN^B6J;LRk^_W5A%;F%WFEm3 zk=W!C0ar|sA}TiKpzf2J>$4foT2E>M50`DoL9=fu{|={N;qDx1?&aT*qh;jo9CY)F zuhR;{oY~f^XZ|14LNAp<}vot>v%)s8*-=1zv+pD=kd+P9JFW5nGcUVaQe}{-rhlZ zW!#g4s?1|_sPEKQ-Y;y`;-!v3%kvh9EAC!W+FYn@ljxqBZM~OAf{7QIGiVt zoDudZtN}5EJ9Chfckx^<#PQ6Mh{KHR%t7cCFsJ!U3`V7mfUN^NbC7kb79)EU7u%L= zRzQ)Nn&D;5-8oIPHJ9nT27vQg1TTt2Qt)U`Zp%Tm6{wTBpt@gRy4sY3P|L^BVJ^eB z-_9IFS?aaVYyOrytUblKGY4^&i zSx$}6P;P4u9)y%p(I;7m=r!?Ht}0}!xrK@6kX zp1WN@>e_0ZLSR`WoQ#MDCS{cu%fxM@nmvO?lw(lvnVJ?GOW8h*JMSq zU))}RG!+*XU?WAvAG(8FNN2oMXin(o+XKr!2we( zJwE_no2I5ZUU-{zr6FW1k8!MCWu>DpJcAboELtkBq$DAAy&y`Z`GvP?m$s>wa=Hyg zLoYn7d59?ayYdle%xNSxvpCz`3;)Z6I@|Ic<}$vAXds^82VQ`BC-M$NXJTYMqy(dC zC#);TGW5cN>RgRzA;G1&&0Q}*ZzPdg<;PS57MCWB_QG$Z{{<+G#~-ClF7G+%J%g`> z&qX&vV<1TcuR}%%PLZcUFv7XzgVXUuP*zHYcD?|mQXKh83pFUk@K8)5(y`+O=##y? zy)B9{uzz~EWBUtGC4~jFvJ~rEC`9`Ii0x~(rq4C~Ow*;NVB_C478* zOv4%Y0Q^IJseZZsq56%1zefbXe-DHMhXZct{y*n`+JDKv-{0W-SKsIH7QnNF9@&9BG&WX4t-CkaW$c)&0!6Myl>w_N(C}(h>9wi?&96q4?#e2y zd1!=XmOA59454QQbUP zu~S;|ebq`2Se1qe*P3>(RcY98txI=XmF|(H_31TjjZSoaLES$sd1%&5+q(2XS!w9m zHA?rEl|EUsbhoMW!8~la5y1)Qiq)NrNNeTeXc~r0Y;!OV!|wX4DIlvee?kM8*3p_o zvfA^o^D>943V#{<$WYe)JgmKvQB`S4Z7VsX{dt&u*HuG}CX|QmS7c+U2FuAdkf>JNJ)rZfH#}jx9E)D@>63=)>-@=!|{yYE}!=%ZwA~sA4 zE#?{r@&IC(?TW?Z8W3p7A&bD0v(V}i<|^%Z05XR4H&z)=%_Z@`Mn#=a9#D;8ZH#qB z!_m2!RLtB+C=ckSN`)TyG~!k>S2&PwtvZnI1iX6jF2RzyN_!q4P?dp@5eeW}3@`1O zW_WKN2obM_mp2&?L|z1^k&+~w0Ahpno=4)3<~(2`AQ2=AnD>{d0_C{gns1g{#|!l{ zjIir5VGit1MZ}8l$pa`77lvVTor^49k0-81ri3=wok)b2cIAN-X#g-1jlS%G) z=VQ^u4SAqN1YUv%h&G`kbJ?wVKtN(6>x1M>da~omJ$Z@#K%LVzrKGLOh#O~X9+u2< zTAcPymAfYoGbLdSMq292jy#|vA-)-gJv1MSP9eKB081n_n^RDVf(#Gk0VfH0YVArS zfep!=5urSQCb8?*S8-1sU{l4a@F^BTNB2A=wrSkz`|?1Vgyq9PnoFKT3KzV}fT1X0 zYD*rNlVCN|LdfVA*!)A``RLW~TOyr#fKJjhpe_dF!^gAisT%_{HCSDETiMqhSGOmd7|xy5MqcOalP``u`)g-?KG+zUe(p;iirzcjKSI{(rr(zwymL07?zdAr9bRgRB0_^&hH# zw*Er>fqH-7mB7aX&jkhpyZk@!f7Sm9|4;fa_;(;8;IDn3_PyOV>3hJp(fbYWi{AHn z7rn#Y-Cl?1uRI^|ybT!vHoIST|F--6?v#7j-R`b){e|l{TtDVI>)PY|w(}32&pV%X zKIUw9HUSOzgSwB^JzIC55X7MMxg?Py0QvJLvcIMe#Jgz1qgX%6~rN}(n3!GGM{ke35z$^ z$f@5T;EkmwCcy$~`c>~Jz<4K+o+xgVaBNZBuIeZVmawq^QIYIFxJe`8E%)vuOn(c} zaBO=4qG30Gmw=r!5xzbFv!E7OB*^V7KpGUdlT{)0NYC~HB*5yl#3VNtAo8*FW3!?q zttgM>tP7+=l zMi|;;vi9nnEI`l`d2lTW(X!X3u&>Z8CW~Ul!ifn;(7HcSd8pVu1&D%eSV{`#p=W(y z-jk@j_Y2z#kpAxDFJlXnef+&C%#939TM7{Uj#8lt1Cpq*wE$ZoNo}ftz}4BPB}z0G zAS2RJXq_mx`^heOUc0vdVX@0VOj*zbrCkL`h}hf6v8eS}NECXI45fD5U4Wd3%~@Ga zY+fnsP(jFymIWoWK?jg1?k_-2qzl9lp*P5yo3K8Z<_+SRw|jd50-~5*><$@sX{f=5 z*7gE~LK3qK@rFjSVsN_6E`picSCHail{$v`k0e?O5Dy7~s8ZN$sohn8tjPL68BZ)j zAzKQN^dzo97YEp$7@%efNa2BUg~Q)lfRsmzYOwW>MrRPve2D^owih7I@d4Cb0q`|0 zH(ZRW(9QzHH)c1cLb%BIPEy5!1&C}k>E@;eRG9brI1B1N;HGFpg7DaJ+XQ)c>@HDj+lo(ugUM z9ld0_gPN0IFb~Pss)+7KxHS*Mpw!dVoG_K{4(wYphabqp{0GU$lnb{zHi^}RzA2d+ z8=W*xd>{|0d5tP%Jb7pSEo#2avovS|N_Xa$tc#hk+MRhw-*StYMr8(Rcjm8K7c--? zyYdjN1+rq;iPB|k6!PCxf0XRPhw@KJncuvCLK_*LLw5J%ZgKSbZY)M(Q}FNMrQDTY zuw>Ph2BWy9RH$K@FxP`}%aQwhUuN6$5siznGpFu}$xF}wwB@H{hxLu4aPbZ^ye7*}Y6Da~R9w%x{6sbS zfH4F#D0hyeAFf8f+p2sh|D?Pqbyj5DmxIcXsTkP){ACTWRs%3r7*G{7-rbsiLR%xN z8f9=yOa5_9sIe+;MhN!iAFH?+4BB8X9JJ}#IG&r<{G+lX6>G)%V3~B)k$*%2B2}iw z$`0L;zf`l*fOVyZYgX#Dszmz#dD~ZQOy2k(A_(J1T(EmT%@DmLq z4Yx!0|8Mm(_5Jn1!1n`R3jBH?0}p_s0lWVX{J-E&`p5iT{x|!+;rm_R)4mJ7gT6O; zU-f>;`|IAH_Qt%MJ>T{Gn&&xBAKnLWxnFXB%KZ*^)P2gm30?qSa{a37S=S@3p$cOAdvNJ0DGZ~tBUg#9qOx}N_YF9Pv= zXo(za649CN`T02D`4mJ3PM88d4C9QDO*CmY2$?Q8D8EwP8SxlCQWe4BgYoHL7}T1J zkeLOK{2Jo;@3U%akIB_ikkuEkfG}vDrImSg`JRAB~#VZASrO zv*>`ai2imcm%6tA$ysQ^3GG!;o^SqodjVpzm_O5st2 z8iKLdo&tnpG0lcVGgc+H7a$)?{o6QqrdD?pARUYGF*F%bTh~--cLBn&*i*(?P-ToG zy{6hUjnu>jcNPF^TaMIJY)@ga`bbSBw-=JCRnsmp71~jNl9kklV#d8b^vP z!Hcw|0O_1iGhDB5cOIG+D3Lw^_ZJ|R^9yF)#mMCB`1QD48vS(Fh5@$}Af9tXgW8k8 zJ_(z^Q`hV!I9PyiE?+oW02mXV8=j5C5HcPM4-kyR_>5^xwHw?~P-46{YGiMGBFXa2 z13y=Q7%!g7tSmJIce7h<_TB=-eKFBur0)c^ z-3OAdP49BdF{!7nYqZ6{2-`* zqAuxac#WPL?!*6klcC)ONTd~GQ^s*a{J`!4WYLO(We&H56W?2a{7LKN^Qu{7zbX0X z{H6jVOf6r3uwso6s+8k~ zEJAJyg5>tXklO#&#VFUu?!r06w5?UwgYkQ z)EYsmu6?QAT{t1^b6zhwk&yS#smMfXdU~Y$?!s~Tl?~Ssiv$ES{-4HYko)hW z4L#8Kp9=g(;HLw3`9I_j`F`6s;QcG_gy)-{v?u8PbN9R454rx?^&!_|uFcNBc0Pqn ze$Uqp*WKgzC*<+VI-YQZ>_4#ok^O`A8}>eXz3s1UzlH|He_ciJ0U@+&hs*3g(cOMd zB`JilIvNv<^kDIfI#p}e|U5m0d-!35JyQp6N@Z_bF5aUntW`Nl#IZ4Wdgz8RH@ED}on{ zP@s=je^V4PyU}Kul)Hbat%!5ZhEVRsAm2Xy@kzW)GlgL;$QvD|4CJ!IQ!Q;p9Nrq` zWyU+`rlS1LB2FL4K`eC%v#*6*(pkg_oHWm$l%TaVT{P%+4vPY6&La&Kaopt8n6+G0 zOnSa5(6Ob6L&%2Ca=*;7f=s@%h;u1BrKYb>j{&P<0UR#k$esyf_8EABH5CWf3hMzD z6kH^PXMzGxf&iL;1}KDzVJ18vM0-6sT|DBmjWE1pO;aH_+-+^C*Nr|v`%PCISqRa^0eB~6>CIfP!6 z#3nvbgbR(CT zx@xqc*1=-ysjq4+k>1uqvBhT_THYwzf}I-Yg%s_z2Sn3fs}cAvMJI!k!W)j?IYUN_ z*kbZVai7n2ad}&;?Ej48w9&K392dcH|-_iQ-vqv6_GYq0XqAUzo|@5SP7 zT1&NkFwDy4epG`rG&^71MWeh!ALSq-AF5oe+8N~waIIadjdDIR6-~{HVWx^ZX_%X8 zwfCbMrm@|t#T`D|pm8|eNoi1_H0kUArYn`hp%IThbvUra{NGK=$Lk>YSalW__INVd{J)c#xLHl)f zo($Qf{}0%H#n$wdreA2f+_b&%2aTo1pKP3Oe5i3}V_m~b4WDkf*6>h6Ga>*!Q~%ET ztMzBsq3n{&GA*oFFLL`+U?)Azi9sn`$P5y+n=L5 zrvExi@B@{aSZYGNY0YZ|d}cljw`sDucL+^b_pUimx?NMsO|{r9$Fj=t60F-vp@*8P zC`Tb6EenWrgfQAczd~Q8*OhxBvB-3E@mL9l@24zkOShq#mAgtXiA!L_)U+C-UYiti z$T}ilO-Bjl@vudsJjyDSj+S6B=YSY*($tt5rNOMC3I!dswTlKV}V*BaAv!}ZUsZv)7Hg;`VL?^)So654%vH?Tk=He2rE-|38!aNQ`(Uu~-1|)n|R#Z*4 zsWiL?sI<=jEuKTJtH=UaCa?oG9Ko?>3fwc%T7(M$?Z#@QafQPgrHXGU!kK`Kv|g{l znu}h5E=Qpc6yZi7Y)RS)RXN`Kql?tO76Mfp)bB0A&4AxC)6e?g=vdFKQh^^r1(oj6 zTcgrbgr5S3lo|RmJbtT0;LT7@#h{tdV6*h;q%?gCG>n`$mub87505)i0j;2d_wt#R+f_X zrAUGeWC#0-kHcAvEw`0rNlaFjpEYdC!Qx|bo^2DdKg>Av^Lj8?d~}T>oz2BZw5wFj z0JQYV$?EOsDPB_SQ}6g_WHKHz_Iiyp^4p5=!V#fXOoZ!eA%MI9?c0j*#o;n=EQ?LX z6G#{`i870+oq}-X9R$yllUO4Q!XOjgu0E^H9h+G=Mtwng? z@D8wPcJyAY(E-?UHx}V*v){Z&#)ikSJqG*t72!`aQuZBzRh9GC-`-b*Qw=Y?;U%|T zO6o^jY5fbotq9K>F+GL@#%?J@1^!ppBFBpGs40^d%TAcg9$+|_%4gt25neVa^Qg#@ z*(Diqg_p)~{5leDC!!+OpsNV)n=*4bS6`#vo+A8l%G`3P-a*Z_uF^@6{@-i+6UzVh zZg~IS+4z@@ztWg&+}7}i4L2Hg)c;ZaTkChh=l|V-M*~~^f9L-rr3;rJx7{2jA@(_XMA?W6X)(TyL)e?29fLm``J`;fAlD{eG=aR8%V z<8U1=;e_rd)}n7_O1*@=x&|}ZQo^wm$4O&^W00?~mE+UX1O4symvAPRcu!2ymc?7e z$+5*qcy4i)x`_lwoLWk}>Z&@89#bqHAJ`)$9N76X0-B&R48N7sT%X~28gVQL1Uq=T zgrhv7b4)%n)Bz@}Hn8-$s4WacWvVG5iySOTZ1xbB7K zBm1?z1b!mmi3BhwXif;g#81ek9DdF7A1r}u2s)}zxtr{X5*UO9UxKV)m5{7EHl+fw zCWeE{OC3p&6GWUkva0WX12DKN23|(m{!PQ49!Ky2wf>pZfKNhUWkie zsc1~(`%VV0A^SA!!4wW9W9^ec`fd^C$>~IVUUY^VIZ%Srko1mHMsKBujamoZZz;ja zPw-GEMyha66j5Bl{bFkgjzvP+G#Ajcu&pKd6jhWErfxwaSb{r{JSKW0R6--NrF6`q zAINi}_v5HVD=46m=qtemMTjcN6rzZh7;e;3fAcC1#pZC7YqQ(-5`0nQxlsTaucktP z%vdj0Ei%eQ42QyqoZ=yf_s-r-97OC8!_GgV?d$f`zHsa=y&Of^(9IwUOQ7GOYx zo8MnLpe{m9|K}k|n5w**ftN%QMBu&>97V2+(`#y2%}F^0b-^JI2-QDU3TdL9sXh;N zjmif~ZQ8b0l`F(D^Yga{O0DauBK>K3Dng|ejX{}PG0cXPEx5vPX&=@c_o)G!7L#fS zcDgm3roXfoiVc$#7D8fIfAT2SUbpcQZ1zzLqUHVRG;B&bV71APmV(xOt(e?Sb#T`vI#k*UK55ZcKGXV~ zub<**X^V9?E2gwV;nQn(bEE|G`lLlS%ct}fXpWZdweD!eeBJ`lp%TpIYtQE`Pz;ss zsbFbFwoj(nPPoF;B=YB7rH$72iy9Z#;0ufblnnLmwyviZj_YZNiB@m8w85H%@)i=^ zgXBH4geJ90wYPdp>CHy55m|F^`jEa=#EnVqOS3O_l%bE>zgNJ4<(3Tgi>H zR8foc|FgC~wKaXJ>F1jsZ2W%Xml}VyG1Yjg@$QDdZ&+>kAguqP24DSO)_H4Ai zP`xwowLmHGvw?{~d%)%YW4r+n@wfTDjp+aX%a`+f7QUB{e%?|g^zaNS?meY7rxoc|jTJMjNFZaDTk8tmV& zf5HCi_Mfs}u?Nwy*YV$(RftM#SFk0OjILc-0A5IuCiQ_g*BY-Z4y;0iVkJ?h1rUV{ z6t_re^D3mB!z8RQr3rHT9`02!=>6?mRw2x=%xn5y3IoIa9a~o+tq6RLhNTG5O?egi z-*9io=2eI!Rh#H--@Jvz$z|QF|QVcgu=BxN_$pu%`(vl>LuG(aW}GDE}94Yf<$5n+#OFP5|KsKwtVn)iaXS? zii?jyd)bloRBB(Pi%skw!bR6p=fWzku6e#?7F54_!i$S@>~p6Z$3P2{jmul$_QTi< zwU_V0Dy}a}!bGGN;Wa>+(N*mZ(|Rl|`qinf6(2XciaSgQMYlk7&7R^KW1=>VXg`!u zg2ct2OZ=GR;EYaQPJ$QA-@S^P$XpkXp1bAS0j z`}k_T5{NaxuYWE)BX_DP8XQhyT0+4!@`(ml0~RAw6U2jWUWG#x^DwWWUlUP+V}Kzf zC-GKiM9kChD!i10CTP`38SHgSR`v&1;mCBJ`H60~G7P+XZccZ=ORljkPprDIkBAsw zc-r$ufT;jStr-Ys(3nw?W{KFPU6r3vg^tNQU(2X-+} zLlWDIRSDD{rWYlXc+{#k@7%F!S9Z{8ea|q@kT)IzN86iMZR)bp!Z%Jwr{FCknj6;3 zhDy(B0<{9zL{VuwGhL@3-c@>=6pVGD27>IhB^ds~skud2O}>jRYWMc^c661VSx-HN zZ^LtXy#poqJ~0AVv%S7Sy~^Rz(^7e^UFF8YT%26;^qL1sZ?SGnj;ajRrE%>oElHbm zS!1e-p<|)a4LRA?Wh(|BDqXKxp<Mp$d$f-(~s#dWo(l$64b?(+!YRydf-UzYe_Z7C%b>raZ8iIK)~Dz%{L zUVBSVshQLn#6T)W$3(+C#R2bVDJ@8yNS{e|H&qfX+puKijsvB*hMlQRF3v4D4$?J) z?Qs_k-Z%S7G0miGu3{RLKwr0)=9Ohdn=hc#(9{GsXODxv(j1QN2;D0Qe)~XWwNQ^3 zau>I}eVSz&M#2dRB;X{P!@Do;9DHdLBcJG_FtJPu~!p;Dx31$i9I z750>-)DE53Gh|3^C6B&%W_WjLLKxfhm#M}2iGb!X9;k*v5kkeeQW$e0c({0SMP7(_ z=bBH5x(;YV35STI}Z}`uK-*0$N!4s~-x>JN!2b%o4~5_O?~NIFV+P)sfj4I0jTv}j2Hu!~H)i0C8F*s` z)-wZpSK*pB#2~&dw6o`AMI>TJ^D2Dwgt?-uEVND76&L`*ucwE{J6GY4#}Tpye`rG~ zR!b57!XOX8eqo{5wF*}}c9N@IYxnAD>AXrtu=G6h!V-cmx5#3sQ(jpS3N4WN{ z_G?~7O0|KrI9ydJx)zEzw0cgKqvdNku-aDv0GMiMIa>@>POY1pJ`a)P%<55}ZJAwj5zL%G@MxG`KZ;(JaH0;emP1QIx7NQW zUmA`#!$<}a@zg@urhjntkOa>4Bcz$6aZnju(%`){0OT{Pa49UaZ{wsT7AN37j&;5j z`!?zS2W-D=Ysxk?G=92qq~Qk*zue%h|4iT?1Izw`f7bV!?=!x8y&v^n@YZ|&##8VN zd79zl|EKPcxZmm?a&JW*|KD@H*EQoh?D9EZa=z8MrS1!L*Xp)7KIVAbQE&g0ebL@- z`zJK;y8k;p@3C!o#^!LaxklJ*783C*^a$)6<)w+t%_Uod(|FozDmbwe#OqAbzS)+< z3%gT6yfK^%)eYADQ}3L|w(%)YLR1cNbclb0vr7{Qx2U8uDdLDtM24Lky04-;aS&nN z#{X0$F)hr2Fc82E%K$>|!34AfXWXSE9zLX*L9SpV z^b1~(F3wiY4&GD-&kfa$Ij;=?^4qxVDW9M8`TT|p-&Qqj3Z@*SUz)6U>o zC?9^vZ-4T!i!|c_b3Y~39y1N`Xf~X{VK@NDN}l03l3>rlWD1$WXJL4jXORyqsHejh zXm&U2vnwgfxi)qUvuty7OZpUR1F)QDG;yQ(JdWB)w7{-g{13Ku6 zp!MD}@=EKlb?+@pzuY`jIV+YWUkaV8)^E%5ueJ=LJk7V75A_o#k6k0QPP;~4X*pZ1 zu;eIOVfV-@dmq9G@2gy&%AzmtdJw}qilT-^uPpXb%Ng0cS_}0PypvN%7@B`|*Xc@{ zl|y=I5;9)r@?GF~ddRhwj*f&Wk(iG7@f4Om2xpA)yr1K+%mvS!3F5(#WModT4lU1I z$Qkgyx8;Fq9k1-st9$RWp0~=PFP}c-vF({BZcSD_WM&8sC5YD{pacm@r@_sxqqE}e zoFEt>J&8h$j>0{(D3FlT<9uxt4tU#^eKxyYj~5W;l_Z87UJQo$RRbP=OnU_#oEjH9 zn8hNoXEYc_d`cM4AV8?{onC+s&*tR;EbAHZ5^hrc4B~0r1S08Bej$p${yoVch=Bjj zlU0zW`5I0#I34l6a55BxQ3@9%g_JFV_XWw6cMStUtSXfTv&69O_BJkWf!|E$N|Vi| zH5uFu_YfF_z{VkHC6TKgzhN%WxVmiO3nU1N>t&vNKPZzZjWn8|Zjr5WPH;(bSineI z(dOh_YKn?YE+PIe7J)wu4T|3Tm3K!pJ~nlD4=+CmC!!rI*Rd`z6;H_?Lnl}Yrr=bW!W_-e z82GANq!KI`;%1TyW}I=gH5S7&Scyq&HT+nNUxgbH{fRr5R+z@L7?0yPitUR#R2^Zy zn$RTA9xrGRH$QKQ$PEy{vS1{0DJg)#zY6!e>WOdXY$NQu%k5YLdi56ji`_d+hXwX% zEG~AC;1>(COGtN#77_R*L$LI75)98EMv}78;(<`d=0K)(nI&7LlLUi9gB3UkA(EJ`23BQ%~^qA5fy zCtS_MX2FQnzU4`U3`L|VZqXpAVh>Ev7qI3dXAjOLJ=hz>PR1X!0ys`MaY5mFkaXQ~ z#t(TXj!h*62RfbMCFByI$ECH7LP~(^+FW#U5pzM<10`FAm^i-_T;zz;q|ZLTyuk-y z1S!pI4$?-0tYDMRnf8lpT^wwUFQK>k=>@+5&7}pwz2M-6ZnOr4b6gxp55MaPq+78) z;jz&Q&4av;cz~DynO{ux-t}{jcs;h0OGd72I6~Cdq>#jT(Qut$!^e^f@dPZ+w7xhz z*D!&2wG0b|+!S^-bqBj72vsFPY+fC7 zY@mtMVvra#mJhrI-GBrSM?t!Prb4cZuIGnc9@~CWqqNTV3mGHHS{W1{8n?PelPPQ| zI>g|Dbmk>vSx6-o;>k!dXc?$U=H?hMZh$V3$+{RpGD=Q;Zu^{u^{0f zMl-K7OUWOXSnB|NnMfQ<(@G1*NE+|3sLG}syb_;C`frl{f7tdmThnUOLQ`www;Dg& zxY)S0;VTV4-q2hBclH0h{#4)}0?!A=0uKLs{AYa6`5L^x<;{49y>IsXrRV*gi|+q& zf7Bgu?{P%r&O(P#a``sgwY2kSdnZG-Kla~zWCfnw?W`9M z>*}B6VP>*m2ueMyL0ax{CO)#V1>GgQM5)#{mE79Iu zUwvA={>JsSZ=np?zDT3Ldp)DCQv2`<#Jk$FUZv)JD{o%kxU1AZwsIHh(|W5JHqV>- zv_h$~eZhb8*a}3w^;9m`vTXma|K^dEJ1}FH*4i&tRS&M*j_D$pH?V^)ZK$Cx2@G?xt=Aa2zNS9k)mFuO{>aBR&ejFJw;Yk4=ukH3d0bl2z*Bh z8s?TO1(1;KkNh`JEMM^0c3nh8qmXK@$`=}4c`Vi_`Gu?ZECVj-IuAk3AY^KSrOd$qLeQ7nW+lF zQ{4BOE^ed^Iae16(b=A505WZKf+~bH{f5UKr}P>E{QwUluY%C}L$*Hz`&$N1vxA8A z=u(-vWBLKMsDF5w&>K1tN&+)imdC-eCgC_#Q;AgOnsRel)NEfqixV*-=hG;;V6>Xv zW1g(T%Uc0FJVgD3f&p}ZU0{>qU#UNcFF-&%vkSG^_5tUKv&*>b_M}h~2Z`iRQ%x={ zpvZagHE9wF5(F_5AriWh(1+UyZpEvM1SLRKiIbW>6}bkT zT1sMc?az~19|lNE+gD#-D9=LDguDv_(F~B+MKKK_=ZWQ$%TUEPuFTtPb`Atciho&y zorCm7fHG_w`UEo2L9NoLl?uv&A*-;#tFicXN-GjfQY;c6gR78HsYj5uf%wvh!qwa{t^wUH_x^ase#z)LU!D1N;0w>`Mrv|`&dgu{JbX@uz z2UX6cWIyJGw3E4TDu!tVIc(x9k;OJ*lQ27BBr~Zf!3$DLPKNxp4-YN}Jho24I(nh? zO1MaA*#nHj#LzFOJTOQwm?N|jje!8aqOCg_MKD#s>FZsFmbPa`Ap~k(nG-X>1Ta8A zPK?VaKqI`c0G(HS6KZ_y;bkahErjLU8DW5SLE1Ht0i>Iaw-H)258NID2L2d}cW5*;G1GHPp~m5XW!y-+p3*1}&p}5+3Qm5imfounHv-@+e9T-k zJT=AW16C1Ji598^zQ|33^(IjplZayhRqF}whR zp}&cjcrp=(2eo){FnN9s+H#wwuo-VZeb)jyqoof+Z{zIGg05J((7`<)>Rz^aY)5gc z+J!}z-Z>(LUaou+7*#9@`;%1ku~ngiLbD}sgnAls?7J}dHtazt;&xeAg=Zv^!$3B% z#ZUu#Mvf2!X!cPV?O>od6MRi#qc284ZfxKN6eKbPp*B|UR0L5nOIQeD6(dZQmm!G* z&1@6<98tK56hoI9rwX_~J$B7&+qB#SN~097zL-1>MI44!)`Ow>!`uo*GzlgRRSicO zeB?Uv7vTVk4YEj_4%;UfUl7oDDB(Iil)^<$y*o?rK~NsM8PRyS>#DSKc63rsqx$&_7-kh<*42$BAOpY7werq4IM3;F+dHU2~6Z#G_U z>}hm0e7@mk!}*4L>%U(AVdVX94SW;c|JlG$V5k58^Z%~@C;k2YCf}d=-tD{K+vWW~ z-v8l!oA;pS&ppeYBknJ|GwwTFpK*mYR=bJKo|La_q2w5pM%d z*pJ%%w!c8rxA5P-G8= zCvev)1kdSuzBTRk*tXJ$Df?h#Djq45sc=V#k3;rj{+lCdu)3|wWUHMB z%zSz2`-N^CX*ifo0S8e#7TdmW@5-~-Y?w#8*lbdmlm`(gjl7>jwx3>TUI8P6!4-^* z&>JZ|S{7|tc?L!MP*k8Xnuu#z`qBzQ2sbQmrCvnA!|8;hY23kJE_xqyp+@*U@QGef zIW@r(V8S0h&*Bf5A7Pg0^uWr~5Y}fvx)*0Zj_2EAivZEVT!<-5MIsArd^ZE7j3*dB zCQNlM#S)S-h$Tbzciq;r@)nFK2})r4Qz8zWjZs98<{z9J=p2bxz`c=>DUz5Z&_jw@ zB-OrW+v6*UM%)jki%C@GFZ5|16-D|0V)##`*x!W&PNIXah$I1K$=NUg&@@y5QbK0Y zL<^#SaRm{K8<#f{{Wvu*Wlb`?>D^QU89-RgK!;n0MOShAa9zkou_;XDLIjitR}kvB z34RTr%)*@Y3@8Z&52VhCn zWsF2%%+=&o_zlpMXv3nvu{3d9=@5I+*!T)!DmU}AYy6#L=koQVn^&VkRv;1zVW~CV z1duS`@GwQQeQ5=8n46Y2VGQtUq=mN}08yoUjbtSuqtR z^qKH)Y3AhVH|RrBaYBRVR&bl`coyv$UWBl0kk3wp%rZ0pgLxPe1PN#g^G-|oT!6={UD2+8CxCPsstw`) zCjH$^L+bA4+V+V5X3I(hYtW0kPOO2snyEY>nIkI~p`e~YTV)EWxtbhpN@}l+sfSkJ zMzjqFf}}Qi+nHXpm+C z;JoFA)sXE^2$=+0wgfILOH$&x&Fm}vw7uyNS!3=NhQc!JRvkAmcKez zPM|Ba@Jbaim*xBenVu!3) zwZqmG53jVNB6YTEMdgU9JL-n)ZT_1_SN3C=kJlb%mAZ#lLbBDWYg?t}v6VKDt$hHk z_E$L8>YqrD;_@xkvC@joOU&`K$wyZHC6S4;eb}|qg45oMO2hhggK;2k_!G+r)4#S$ zc>f+=*|*lr8upMhnTG6JX=eAXJ+oEn9$sl)Yi6s|>{!`@n#9Sg?gZw($~!k?4-ymJ zg|70gSpA$;R_j>VDch>L{*{$T|G(e%VO!HDo8I2^Xj60Jw;PulPd0q7;Wr!R8#?O$ zwLVw>RQ=(4Ti~;Sp9nk@sQ3S#|9$?e{-b`6?+d=0z9C(ne>gMWpIR4D>(~gG_8}JAAjQynTPtf=} z{%cOdBX$Gn!3K33@ZVwrz!oWK6 zq}1%0NvTiQL8?DJfSDyDQ_Y!OyH7poe#|eqTUDLkwTV2GhOl##Zt&AATX)Mzkf==A zT$|`w%=oeNDNGYztMb*t_0I8&Bd;Dy_sYs@Hr3CyX7$C9SB|86FspPc%UP8_)~@>U z!E`s~kvAH|@q~chOe0liRNoRWj=Xd+4atu*ttvA&hU;QVpahHx!2mq7Tc~X4KZ{xU5jeFsh>f; zHDL`$BoE3t1QiB?JI11e@$&8WQkk8iSfE z^|SiD@QIOE52hjgay2zo>SwKLuXLm#`)(pdrfkNl-T7 z0Im!=l8TL~E5qtA%3B)-Ne(@-I*jA4=Z~c!2Dh;{0gNt`88-+7oNV;w{fTsn;#&zs zh1~n#a*>84%pPZIa?O?0r#StvF_T%#aFm~?`VfZK!>PbWP-3ACBE+o7#c+yC1CCNeLfrFb3T#3PifwgLg98MT^&WSX6Fk z(eHqt16Y!VODPjj8_rBXeM-h|O9Cg-@G9Lz5|o@6bA2^i)v6y!!>M!wUAH_v=E`Em zs#QIihCk>os(ME6TNzt2)U}ws{xp0-j}lUJ5l#$2q@`9y`+hd2n0v!z42FGSa0%Jp z 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