diff --git a/.travis.yml b/.travis.yml index 497b67b08..2b4dbf181 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +cache: pip python: - '2.7' env: @@ -6,20 +7,33 @@ env: addons: apt: packages: - # for wxPython: - - python-wxgtk2.8 - - python-wxtools - - wx2.8-doc - - wx2.8-examples - - wx2.8-headers - - wx2.8-i18n before_install: - - pip install -U tox + - sudo apt-get update && sudo apt-get --reinstall install -qq language-pack-en language-pack-ru language-pack-he language-pack-zh-hans + - pip install tox + # We're not actually installing Tox, but have to run it before we install wxPython via Conda. This is fugly but vOv + - tox + # get Conda + - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then + wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; + else + wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; + fi + - bash miniconda.sh -b -p $HOME/miniconda + - export PATH="$HOME/miniconda/bin:$PATH" + - hash -r + - conda config --set always_yes yes --set changeps1 no + - conda update -q conda + # Useful for debugging any issues with conda + - conda info -a install: + # install wxPython 3.0.0.0 + - conda install -c https://conda.anaconda.org/travis wxpython +before_script: - pip install -r requirements.txt - pip install -r requirements_test.txt script: - - tox - py.test --cov=./ after_success: - bash <(curl -s https://codecov.io/bash) +before_deploy: + - pip install -r requirements_build_linux.txt diff --git a/_development/__init__.py b/_development/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/_development/helpers.py b/_development/helpers.py new file mode 100644 index 000000000..4ae17bc5e --- /dev/null +++ b/_development/helpers.py @@ -0,0 +1,145 @@ +# noinspection PyPackageRequirements +import pytest + +import os +import sys +import threading + +from sqlalchemy import MetaData, create_engine +from sqlalchemy.orm import sessionmaker + +script_dir = os.path.dirname(os.path.abspath(__file__)) +# Add root folder to python paths +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..'))) +sys._called_from_test = True + +# noinspection PyUnresolvedReferences,PyUnusedLocal +@pytest.fixture +def DBInMemory_test(): + def rollback(): + with sd_lock: + saveddata_session.rollback() + + + print("Creating database in memory") + from os.path import realpath, join, dirname, abspath + + debug = False + gamedataCache = True + saveddataCache = True + gamedata_version = "" + gamedata_connectionstring = 'sqlite:///' + realpath(join(dirname(abspath(unicode(__file__))), "..", "eve.db")) + saveddata_connectionstring = 'sqlite:///:memory:' + + class ReadOnlyException(Exception): + pass + + if callable(gamedata_connectionstring): + gamedata_engine = create_engine("sqlite://", creator=gamedata_connectionstring, echo=debug) + else: + gamedata_engine = create_engine(gamedata_connectionstring, echo=debug) + + gamedata_meta = MetaData() + gamedata_meta.bind = gamedata_engine + gamedata_session = sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False)() + + # This should be moved elsewhere, maybe as an actual query. Current, without try-except, it breaks when making a new + # game db because we haven't reached gamedata_meta.create_all() + try: + gamedata_version = gamedata_session.execute( + "SELECT `field_value` FROM `metadata` WHERE `field_name` LIKE 'client_build'" + ).fetchone()[0] + except Exception as e: + print("Missing gamedata version.") + gamedata_version = None + + if saveddata_connectionstring is not None: + if callable(saveddata_connectionstring): + saveddata_engine = create_engine(creator=saveddata_connectionstring, echo=debug) + else: + saveddata_engine = create_engine(saveddata_connectionstring, echo=debug) + + saveddata_meta = MetaData() + saveddata_meta.bind = saveddata_engine + saveddata_session = sessionmaker(bind=saveddata_engine, autoflush=False, expire_on_commit=False)() + else: + saveddata_meta = None + + # Lock controlling any changes introduced to session + sd_lock = threading.Lock() + + # Import all the definitions for all our database stuff + # noinspection PyPep8 + #from eos.db.gamedata import alphaClones, attribute, category, effect, group, icon, item, marketGroup, metaData, metaGroup, queries, traits, unit + # noinspection PyPep8 + #from eos.db.saveddata import booster, cargo, character, crest, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, miscData, module, override, price, queries, skill, targetResists, user + + # If using in memory saveddata, you'll want to reflect it so the data structure is good. + if saveddata_connectionstring == "sqlite:///:memory:": + saveddata_meta.create_all() + + # Output debug info to help us troubleshoot Travis + print(saveddata_engine) + print(gamedata_engine) + + helper = { + #'config': eos.config, + 'gamedata_session' : gamedata_session, + 'saveddata_session' : saveddata_session, + } + return helper + +# noinspection PyUnresolvedReferences,PyUnusedLocal +@pytest.fixture +def DBInMemory(): + print("Creating database in memory") + + import eos.config + + import eos + import eos.db + + # Output debug info to help us troubleshoot Travis + print(eos.db.saveddata_engine) + print(eos.db.gamedata_engine) + + helper = { + 'config': eos.config, + 'db' : eos.db, + 'gamedata_session' : eos.db.gamedata_session, + 'saveddata_session' : eos.db.saveddata_session, + } + return helper + + +@pytest.fixture +def Gamedata(): + print("Building Gamedata") + from eos.gamedata import Item + + helper = { + 'Item': Item, + } + return helper + + +@pytest.fixture +def Saveddata(): + print("Building Saveddata") + from eos.saveddata.ship import Ship + from eos.saveddata.fit import Fit + from eos.saveddata.character import Character + from eos.saveddata.module import Module, State + from eos.saveddata.citadel import Citadel + from eos.saveddata.booster import Booster + + helper = { + 'Structure': Citadel, + 'Ship' : Ship, + 'Fit' : Fit, + 'Character': Character, + 'Module' : Module, + 'State' : State, + 'Booster' : Booster, + } + return helper diff --git a/_development/helpers_fits.py b/_development/helpers_fits.py new file mode 100644 index 000000000..61db7a3ff --- /dev/null +++ b/_development/helpers_fits.py @@ -0,0 +1,66 @@ +import pytest + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata + + +# noinspection PyShadowingNames +@pytest.fixture +def RifterFit(DB, Gamedata, Saveddata): + print("Creating Rifter") + item = DB['gamedata_session'].query(Gamedata['Item']).filter(Gamedata['Item'].name == "Rifter").first() + ship = Saveddata['Ship'](item) + # setup fit + fit = Saveddata['Fit'](ship, "My Rifter Fit") + + return fit + + +# noinspection PyShadowingNames +@pytest.fixture +def KeepstarFit(DB, Gamedata, Saveddata): + print("Creating Keepstar") + item = DB['gamedata_session'].query(Gamedata['Item']).filter(Gamedata['Item'].name == "Keepstar").first() + ship = Saveddata['Structure'](item) + # setup fit + fit = Saveddata['Fit'](ship, "Keepstar Fit") + + return fit + + +# noinspection PyShadowingNames +@pytest.fixture +def CurseFit(DB, Gamedata, Saveddata): + print("Creating Curse - With Neuts") + item = DB['gamedata_session'].query(Gamedata['Item']).filter(Gamedata['Item'].name == "Curse").first() + ship = Saveddata['Ship'](item) + # setup fit + fit = Saveddata['Fit'](ship, "Curse - With Neuts") + + mod = Saveddata['Module'](DB['db'].getItem("Medium Energy Neutralizer II")) + mod.state = Saveddata['State'].ONLINE + + # Add 5 neuts + for _ in xrange(5): + fit.modules.append(mod) + + return fit + + +# noinspection PyShadowingNames +@pytest.fixture +def HeronFit(DB, Gamedata, Saveddata): + print("Creating Heron - RemoteSebo") + item = DB['gamedata_session'].query(Gamedata['Item']).filter(Gamedata['Item'].name == "Heron").first() + ship = Saveddata['Ship'](item) + # setup fit + fit = Saveddata['Fit'](ship, "Heron - RemoteSebo") + + mod = Saveddata['Module'](DB['db'].getItem("Remote Sensor Booster II")) + mod.state = Saveddata['State'].ONLINE + + # Add 5 neuts + for _ in xrange(4): + fit.modules.append(mod) + + return fit \ No newline at end of file diff --git a/_development/helpers_items.py b/_development/helpers_items.py new file mode 100644 index 000000000..7ae722e23 --- /dev/null +++ b/_development/helpers_items.py @@ -0,0 +1,12 @@ +import pytest + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata + + +# noinspection PyShadowingNames +@pytest.fixture +def StrongBluePillBooster (DB, Gamedata, Saveddata): + print("Creating Strong Blue Pill Booster") + item = DB['gamedata_session'].query(Gamedata['Item']).filter(Gamedata['Item'].name == "Strong Blue Pill Booster").first() + return Saveddata['Booster'](item) diff --git a/_development/helpers_locale.py b/_development/helpers_locale.py new file mode 100644 index 000000000..4d4d29f4e --- /dev/null +++ b/_development/helpers_locale.py @@ -0,0 +1,101 @@ +import os + +# https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756(v=vs.85).aspx +windows_codecs = { + 'cp1252', # Standard Windows + 'cp1251', # Russian + 'cp037', + 'cp424', + 'cp437', + 'cp500', + 'cp720', + 'cp737', + 'cp775', + 'cp850', + 'cp852', + 'cp855', + 'cp856', + 'cp857', + 'cp858', + 'cp860', + 'cp861', + 'cp862', + 'cp863', + 'cp864', + 'cp865', + 'cp866', + 'cp869', + 'cp874', + 'cp875', + 'cp932', + 'cp949', + 'cp950', + 'cp1006', + 'cp1026', + 'cp1140', + 'cp1250', + 'cp1253', + 'cp1254', + 'cp1255', + 'cp1256', + 'cp1257', + 'cp1258', +} + +linux_codecs = { + 'utf_8', # Generic Linux/Mac +} + +mac_codecs = [ + 'utf_8', # Generic Linux/Mac + 'mac_cyrillic', + 'mac_greek', + 'mac_iceland', + 'mac_latin2', + 'mac_roman', + 'mac_turkish', +] + +universal_codecs = [ + 'utf_16', 'utf_32', 'utf_32_be', 'utf_32_le', 'utf_16_be', 'utf_16_le', 'utf_7', 'utf_8_sig', +] + +other_codecs = [ + 'scii', 'big5', 'big5hkscs', 'euc_jp', 'euc_jis_2004', 'euc_jisx0213', 'euc_kr', 'gb2312', 'gbk', 'gb18030', 'hz', 'iso2022_jp', 'iso2022_jp_1', + 'iso2022_jp_2', 'iso2022_jp_2004', 'iso2022_jp_3', 'iso2022_jp_ext', 'iso2022_kr', 'latin_1', 'iso8859_2', 'iso8859_3', 'iso8859_4', 'iso8859_5', + 'iso8859_6', 'iso8859_7', 'iso8859_8', 'iso8859_9', 'iso8859_10', 'iso8859_11', 'iso8859_13', 'iso8859_14', 'iso8859_15', 'iso8859_16', 'johab', 'koi8_r', + 'koi8_u', 'ptcp154', 'shift_jis', 'shift_jis_2004', 'shift_jisx0213' +] + +system_names = { + 'Windows': windows_codecs, + 'Linux': linux_codecs, + 'Darwin': mac_codecs, +} + + +def GetPath(root, file=None, codec=None): + # Replace this with the function we actually use for this + path = os.path.realpath(os.path.abspath(root)) + + if file: + path = os.path.join(path, file) + + if codec: + path = path.decode(codec) + + return path + +def GetUnicodePath(root, file=None, codec=None): + # Replace this with the function we actually use for this + path = os.path.realpath(os.path.abspath(root)) + + if file: + path = os.path.join(path, file) + + if codec: + path = unicode(path, codec) + else: + path = unicode(path) + + return path diff --git a/config.py b/config.py index 884803d76..fe711ef11 100644 --- a/config.py +++ b/config.py @@ -19,9 +19,9 @@ debug = False saveInRoot = False # Version data -version = "1.28.2" -tag = "git" -expansionName = "YC119.3" +version = "1.29.0" +tag = "Stable" +expansionName = "YC119.5" expansionVersion = "1.0" evemonMinVersion = "4081" @@ -29,6 +29,7 @@ pyfaPath = None savePath = None saveDB = None gameDB = None +logPath = None def isFrozen(): @@ -95,7 +96,9 @@ def defPaths(customSavePath): # The database where the static EVE data from the datadump is kept. # This is not the standard sqlite datadump but a modified version created by eos # maintenance script - gameDB = os.path.join(pyfaPath, "eve.db") + gameDB = getattr(configforced, "gameDB", gameDB) + if not gameDB: + gameDB = os.path.join(pyfaPath, "eve.db") # DON'T MODIFY ANYTHING BELOW import eos.config diff --git a/eos/config.py b/eos/config.py index 38371e299..ec64e2bd3 100644 --- a/eos/config.py +++ b/eos/config.py @@ -1,17 +1,30 @@ import sys from os.path import realpath, join, dirname, abspath +from logbook import Logger +import os + +istravis = os.environ.get('TRAVIS') == 'true' +pyfalog = Logger(__name__) + debug = False gamedataCache = True saveddataCache = True gamedata_version = "" -gamedata_connectionstring = 'sqlite:///' + unicode(realpath(join(dirname(abspath(__file__)), "..", "eve.db")), - sys.getfilesystemencoding()) -saveddata_connectionstring = 'sqlite:///' + unicode( - realpath(join(dirname(abspath(__file__)), "..", "saveddata", "saveddata.db")), sys.getfilesystemencoding()) +gamedata_connectionstring = 'sqlite:///' + unicode(realpath(join(dirname(abspath(__file__)), "..", "eve.db")), sys.getfilesystemencoding()) +pyfalog.debug("Gamedata connection string: {0}", gamedata_connectionstring) + +if istravis is True or hasattr(sys, '_called_from_test'): + # Running in Travis. Run saveddata database in memory. + saveddata_connectionstring = 'sqlite:///:memory:' +else: + saveddata_connectionstring = 'sqlite:///' + unicode(realpath(join(dirname(abspath(__file__)), "..", "saveddata", "saveddata.db")), sys.getfilesystemencoding()) + +pyfalog.debug("Saveddata connection string: {0}", saveddata_connectionstring) settings = { - "setting1": True + "useStaticAdaptiveArmorHardener": False, + "strictSkillLevels": True, } # Autodetect path, only change if the autodetection bugs out. diff --git a/eos/db/__init__.py b/eos/db/__init__.py index 680c67e1e..a29bb86c7 100644 --- a/eos/db/__init__.py +++ b/eos/db/__init__.py @@ -27,7 +27,9 @@ from eos import config from logbook import Logger pyfalog = Logger(__name__) - +pyfalog.info("Initializing database") +pyfalog.info("Gamedata connection: {0}", config.gamedata_connectionstring) +pyfalog.info("Saveddata connection: {0}", config.saveddata_connectionstring) class ReadOnlyException(Exception): pass @@ -47,7 +49,7 @@ gamedata_session = sessionmaker(bind=gamedata_engine, autoflush=False, expire_on # game db because we haven't reached gamedata_meta.create_all() try: config.gamedata_version = gamedata_session.execute( - "SELECT `field_value` FROM `metadata` WHERE `field_name` LIKE 'client_build'" + "SELECT `field_value` FROM `metadata` WHERE `field_name` LIKE 'client_build'" ).fetchone()[0] except Exception as e: pyfalog.warning("Missing gamedata version.") @@ -68,13 +70,14 @@ else: saveddata_meta = None # Lock controlling any changes introduced to session -sd_lock = threading.Lock() +sd_lock = threading.RLock() # Import all the definitions for all our database stuff # noinspection PyPep8 from eos.db.gamedata import alphaClones, attribute, category, effect, group, icon, item, marketGroup, metaData, metaGroup, queries, traits, unit # noinspection PyPep8 -from eos.db.saveddata import booster, cargo, character, crest, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, miscData, module, override, price, queries, skill, targetResists, user +from eos.db.saveddata import booster, cargo, character, crest, damagePattern, databaseRepair, drone, fighter, fit, implant, implantSet, loadDefaultDatabaseValues, \ + miscData, module, override, price, queries, skill, targetResists, user # Import queries # noinspection PyPep8 @@ -85,8 +88,10 @@ from eos.db.saveddata.queries import * # If using in memory saveddata, you'll want to reflect it so the data structure is good. if config.saveddata_connectionstring == "sqlite:///:memory:": saveddata_meta.create_all() + pyfalog.info("Running database out of memory.") def rollback(): with sd_lock: + pyfalog.warning("Session rollback triggered.") saveddata_session.rollback() diff --git a/eos/db/gamedata/alphaClones.py b/eos/db/gamedata/alphaClones.py index d18130358..970c3c31b 100644 --- a/eos/db/gamedata/alphaClones.py +++ b/eos/db/gamedata/alphaClones.py @@ -40,11 +40,11 @@ alphacloneskskills_table = Table( mapper(AlphaClone, alphaclones_table, properties={ - "ID": synonym("alphaCloneID"), + "ID" : synonym("alphaCloneID"), "skills": relation( - AlphaCloneSkill, - cascade="all,delete-orphan", - backref="clone") + AlphaCloneSkill, + cascade="all,delete-orphan", + backref="clone") }) mapper(AlphaCloneSkill, alphacloneskskills_table) diff --git a/eos/db/gamedata/attribute.py b/eos/db/gamedata/attribute.py index ac2f73978..20de4d1a9 100644 --- a/eos/db/gamedata/attribute.py +++ b/eos/db/gamedata/attribute.py @@ -45,11 +45,13 @@ mapper(Attribute, typeattributes_table, properties={"info": relation(AttributeInfo, lazy=False)}) mapper(AttributeInfo, attributes_table, - properties={"icon": relation(Icon), - "unit": relation(Unit), - "ID": synonym("attributeID"), - "name": synonym("attributeName"), - "description": deferred(attributes_table.c.description)}) + properties={ + "icon" : relation(Icon), + "unit" : relation(Unit), + "ID" : synonym("attributeID"), + "name" : synonym("attributeName"), + "description": deferred(attributes_table.c.description) + }) Attribute.ID = association_proxy("info", "attributeID") Attribute.name = association_proxy("info", "attributeName") diff --git a/eos/db/gamedata/category.py b/eos/db/gamedata/category.py index 884f93666..0fd84da79 100644 --- a/eos/db/gamedata/category.py +++ b/eos/db/gamedata/category.py @@ -31,7 +31,9 @@ categories_table = Table("invcategories", gamedata_meta, Column("iconID", Integer, ForeignKey("icons.iconID"))) mapper(Category, categories_table, - properties={"icon": relation(Icon), - "ID": synonym("categoryID"), - "name": synonym("categoryName"), - "description": deferred(categories_table.c.description)}) + properties={ + "icon" : relation(Icon), + "ID" : synonym("categoryID"), + "name" : synonym("categoryName"), + "description": deferred(categories_table.c.description) + }) diff --git a/eos/db/gamedata/effect.py b/eos/db/gamedata/effect.py index d766b29eb..012efe966 100644 --- a/eos/db/gamedata/effect.py +++ b/eos/db/gamedata/effect.py @@ -18,11 +18,10 @@ # =============================================================================== from sqlalchemy import Column, String, Integer, Boolean, Table, ForeignKey -from sqlalchemy.ext.associationproxy import association_proxy -from sqlalchemy.orm import mapper, synonym, relation, deferred +from sqlalchemy.orm import mapper, synonym, deferred from eos.db import gamedata_meta -from eos.gamedata import Effect, EffectInfo +from eos.gamedata import Effect, ItemEffect typeeffects_table = Table("dgmtypeeffects", gamedata_meta, Column("typeID", Integer, ForeignKey("invtypes.typeID"), primary_key=True, index=True), @@ -34,17 +33,14 @@ effects_table = Table("dgmeffects", gamedata_meta, Column("description", String), Column("published", Boolean), Column("isAssistance", Boolean), - Column("isOffensive", Boolean)) + Column("isOffensive", Boolean), + Column("resistanceID", Integer)) -mapper(EffectInfo, effects_table, - properties={"ID": synonym("effectID"), - "name": synonym("effectName"), - "description": deferred(effects_table.c.description)}) +mapper(Effect, effects_table, + properties={ + "ID" : synonym("effectID"), + "name" : synonym("effectName"), + "description": deferred(effects_table.c.description) + }) -mapper(Effect, typeeffects_table, - properties={"ID": synonym("effectID"), - "info": relation(EffectInfo, lazy=False)}) - -Effect.name = association_proxy("info", "name") -Effect.description = association_proxy("info", "description") -Effect.published = association_proxy("info", "published") +mapper(ItemEffect, typeeffects_table) diff --git a/eos/db/gamedata/group.py b/eos/db/gamedata/group.py index cd6763923..4373b2ca5 100644 --- a/eos/db/gamedata/group.py +++ b/eos/db/gamedata/group.py @@ -32,8 +32,10 @@ groups_table = Table("invgroups", gamedata_meta, Column("iconID", Integer, ForeignKey("icons.iconID"))) mapper(Group, groups_table, - properties={"category": relation(Category, backref="groups"), - "icon": relation(Icon), - "ID": synonym("groupID"), - "name": synonym("groupName"), - "description": deferred(groups_table.c.description)}) + properties={ + "category" : relation(Category, backref="groups"), + "icon" : relation(Icon), + "ID" : synonym("groupID"), + "name" : synonym("groupName"), + "description": deferred(groups_table.c.description) + }) diff --git a/eos/db/gamedata/icon.py b/eos/db/gamedata/icon.py index 396c5c582..9fd41605a 100644 --- a/eos/db/gamedata/icon.py +++ b/eos/db/gamedata/icon.py @@ -29,5 +29,7 @@ icons_table = Table("icons", gamedata_meta, Column("iconFile", String)) mapper(Icon, icons_table, - properties={"ID": synonym("iconID"), - "description": deferred(icons_table.c.description)}) + properties={ + "ID" : synonym("iconID"), + "description": deferred(icons_table.c.description) + }) diff --git a/eos/db/gamedata/item.py b/eos/db/gamedata/item.py index da43f9ef2..ad6882435 100644 --- a/eos/db/gamedata/item.py +++ b/eos/db/gamedata/item.py @@ -21,6 +21,7 @@ from sqlalchemy import Column, String, Integer, Boolean, ForeignKey, Table, Floa from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import relation, mapper, synonym, deferred from sqlalchemy.orm.collections import attribute_mapped_collection +from eos.db.gamedata.effect import typeeffects_table from eos.db import gamedata_meta from eos.gamedata import Attribute, Effect, Group, Icon, Item, MetaType, Traits @@ -43,19 +44,20 @@ from .metaGroup import metatypes_table # noqa from .traits import traits_table # noqa mapper(Item, items_table, - properties={"group": relation(Group, backref="items"), - "icon": relation(Icon), - "_Item__attributes": relation(Attribute, collection_class=attribute_mapped_collection('name')), - "effects": relation(Effect, collection_class=attribute_mapped_collection('name')), - "metaGroup": relation(MetaType, + properties={ + "group" : relation(Group, backref="items"), + "icon" : relation(Icon), + "_Item__attributes": relation(Attribute, collection_class=attribute_mapped_collection('name')), + "effects": relation(Effect, secondary=typeeffects_table, collection_class=attribute_mapped_collection('name')), + "metaGroup" : relation(MetaType, primaryjoin=metatypes_table.c.typeID == items_table.c.typeID, uselist=False), - "ID": synonym("typeID"), - "name": synonym("typeName"), - "description": deferred(items_table.c.description), - "traits": relation(Traits, - primaryjoin=traits_table.c.typeID == items_table.c.typeID, - uselist=False) - }) + "ID" : synonym("typeID"), + "name" : synonym("typeName"), + "description" : deferred(items_table.c.description), + "traits" : relation(Traits, + primaryjoin=traits_table.c.typeID == items_table.c.typeID, + uselist=False) + }) Item.category = association_proxy("group", "category") diff --git a/eos/db/gamedata/marketGroup.py b/eos/db/gamedata/marketGroup.py index c107c486d..faf88780b 100644 --- a/eos/db/gamedata/marketGroup.py +++ b/eos/db/gamedata/marketGroup.py @@ -33,10 +33,12 @@ marketgroups_table = Table("invmarketgroups", gamedata_meta, Column("iconID", Integer, ForeignKey("icons.iconID"))) mapper(MarketGroup, marketgroups_table, - properties={"items": relation(Item, backref="marketGroup"), - "parent": relation(MarketGroup, backref="children", - remote_side=[marketgroups_table.c.marketGroupID]), - "icon": relation(Icon), - "ID": synonym("marketGroupID"), - "name": synonym("marketGroupName"), - "description": deferred(marketgroups_table.c.description)}) + properties={ + "items" : relation(Item, backref="marketGroup"), + "parent" : relation(MarketGroup, backref="children", + remote_side=[marketgroups_table.c.marketGroupID]), + "icon" : relation(Icon), + "ID" : synonym("marketGroupID"), + "name" : synonym("marketGroupName"), + "description": deferred(marketgroups_table.c.description) + }) diff --git a/eos/db/gamedata/metaGroup.py b/eos/db/gamedata/metaGroup.py index 713e8a2c4..3c044a1cf 100644 --- a/eos/db/gamedata/metaGroup.py +++ b/eos/db/gamedata/metaGroup.py @@ -35,13 +35,17 @@ metatypes_table = Table("invmetatypes", gamedata_meta, Column("metaGroupID", Integer, ForeignKey("invmetagroups.metaGroupID"))) mapper(MetaGroup, metagroups_table, - properties={"ID": synonym("metaGroupID"), - "name": synonym("metaGroupName")}) + properties={ + "ID" : synonym("metaGroupID"), + "name": synonym("metaGroupName") + }) mapper(MetaType, metatypes_table, - properties={"ID": synonym("metaGroupID"), - "parent": relation(Item, primaryjoin=metatypes_table.c.parentTypeID == items_table.c.typeID), - "items": relation(Item, primaryjoin=metatypes_table.c.typeID == items_table.c.typeID), - "info": relation(MetaGroup, lazy=False)}) + properties={ + "ID" : synonym("metaGroupID"), + "parent": relation(Item, primaryjoin=metatypes_table.c.parentTypeID == items_table.c.typeID), + "items" : relation(Item, primaryjoin=metatypes_table.c.typeID == items_table.c.typeID), + "info" : relation(MetaGroup, lazy=False) + }) MetaType.name = association_proxy("info", "name") diff --git a/eos/db/gamedata/queries.py b/eos/db/gamedata/queries.py index 673f1767f..fa8d98723 100644 --- a/eos/db/gamedata/queries.py +++ b/eos/db/gamedata/queries.py @@ -17,7 +17,7 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy.orm import join, exc +from sqlalchemy.orm import join, exc, aliased from sqlalchemy.sql import and_, or_, select import eos.config @@ -27,12 +27,11 @@ from eos.db.gamedata.group import groups_table from eos.db.util import processEager, processWhere from eos.gamedata import AlphaClone, Attribute, Category, Group, Item, MarketGroup, MetaGroup, AttributeInfo, MetaData +cache = {} configVal = getattr(eos.config, "gamedataCache", None) if configVal is True: def cachedQuery(amount, *keywords): def deco(function): - cache = {} - def checkAndReturn(*args, **kwargs): useCache = kwargs.pop("useCache", True) cacheKey = [] @@ -98,6 +97,35 @@ def getItem(lookfor, eager=None): return item +@cachedQuery(1, "lookfor") +def getItems(lookfor, eager=None): + """ + Gets a list of items. Does a bit of cache hackery to get working properly -- cache + is usually based on function calls with the parameters, needed to extract data directly. + Works well enough. Not currently used, but it's here for possible future inclusion + """ + + toGet = [] + results = [] + + for id in lookfor: + if (id, None) in cache: + results.append(cache.get((id, None))) + else: + toGet.append(id) + + if len(toGet) > 0: + # Get items that aren't currently cached, and store them in the cache + items = gamedata_session.query(Item).filter(Item.ID.in_(toGet)).all() + for item in items: + cache[(item.ID, None)] = item + results += items + + # sort the results based on the original indexing + results.sort(key=lambda x: lookfor.index(x.ID)) + return results + + @cachedQuery(1, "lookfor") def getAlphaClone(lookfor, eager=None): if isinstance(lookfor, int): @@ -152,7 +180,7 @@ def getCategory(lookfor, eager=None): category = gamedata_session.query(Category).get(lookfor) else: category = gamedata_session.query(Category).options(*processEager(eager)).filter( - Category.ID == lookfor).first() + Category.ID == lookfor).first() elif isinstance(lookfor, basestring): if lookfor in categoryNameMap: id = categoryNameMap[lookfor] @@ -160,11 +188,11 @@ def getCategory(lookfor, eager=None): category = gamedata_session.query(Category).get(id) else: category = gamedata_session.query(Category).options(*processEager(eager)).filter( - Category.ID == id).first() + Category.ID == id).first() else: # Category names are unique, so we can use first() instead of one() category = gamedata_session.query(Category).options(*processEager(eager)).filter( - Category.name == lookfor).first() + Category.name == lookfor).first() categoryNameMap[lookfor] = category.ID else: raise TypeError("Need integer or string as argument") @@ -181,7 +209,7 @@ def getMetaGroup(lookfor, eager=None): metaGroup = gamedata_session.query(MetaGroup).get(lookfor) else: metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter( - MetaGroup.ID == lookfor).first() + MetaGroup.ID == lookfor).first() elif isinstance(lookfor, basestring): if lookfor in metaGroupNameMap: id = metaGroupNameMap[lookfor] @@ -189,11 +217,11 @@ def getMetaGroup(lookfor, eager=None): metaGroup = gamedata_session.query(MetaGroup).get(id) else: metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter( - MetaGroup.ID == id).first() + MetaGroup.ID == id).first() else: # MetaGroup names are unique, so we can use first() instead of one() metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter( - MetaGroup.name == lookfor).first() + MetaGroup.name == lookfor).first() metaGroupNameMap[lookfor] = metaGroup.ID else: raise TypeError("Need integer or string as argument") @@ -207,7 +235,7 @@ def getMarketGroup(lookfor, eager=None): marketGroup = gamedata_session.query(MarketGroup).get(lookfor) else: marketGroup = gamedata_session.query(MarketGroup).options(*processEager(eager)).filter( - MarketGroup.ID == lookfor).first() + MarketGroup.ID == lookfor).first() else: raise TypeError("Need integer as argument") return marketGroup @@ -224,7 +252,7 @@ def getItemsByCategory(filter, where=None, eager=None): filter = processWhere(filter, where) return gamedata_session.query(Item).options(*processEager(eager)).join(Item.group, Group.category).filter( - filter).all() + filter).all() @cachedQuery(3, "where", "nameLike", "join") @@ -249,6 +277,22 @@ def searchItems(nameLike, where=None, join=None, eager=None): return items +@cachedQuery(3, "where", "nameLike", "join") +def searchSkills(nameLike, where=None, eager=None): + if not isinstance(nameLike, basestring): + raise TypeError("Need string as argument") + + items = gamedata_session.query(Item).options(*processEager(eager)).join(Item.group, Group.category) + for token in nameLike.split(' '): + token_safe = u"%{0}%".format(sqlizeString(token)) + if where is not None: + items = items.filter(and_(Item.name.like(token_safe, escape="\\"), Category.ID == 16, where)) + else: + items = items.filter(and_(Item.name.like(token_safe, escape="\\"), Category.ID == 16)) + items = items.limit(100).all() + return items + + @cachedQuery(2, "where", "itemids") def getVariations(itemids, groupIDs=None, where=None, eager=None): for itemid in itemids: @@ -262,7 +306,7 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None): filter = processWhere(itemfilter, where) joinon = items_table.c.typeID == metatypes_table.c.typeID vars = gamedata_session.query(Item).options(*processEager(eager)).join((metatypes_table, joinon)).filter( - filter).all() + filter).all() if vars: return vars @@ -271,7 +315,7 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None): filter = processWhere(itemfilter, where) joinon = items_table.c.groupID == groups_table.c.groupID vars = gamedata_session.query(Item).options(*processEager(eager)).join((groups_table, joinon)).filter( - filter).all() + filter).all() return vars @@ -315,3 +359,25 @@ def directAttributeRequest(itemIDs, attrIDs): result = gamedata_session.execute(q).fetchall() return result + + +def getRequiredFor(itemID, attrMapping): + Attribute1 = aliased(Attribute) + Attribute2 = aliased(Attribute) + + skillToLevelClauses = [] + + for attrSkill, attrLevel in attrMapping.iteritems(): + skillToLevelClauses.append(and_(Attribute1.attributeID == attrSkill, Attribute2.attributeID == attrLevel)) + + queryOr = or_(*skillToLevelClauses) + + q = select((Attribute2.typeID, Attribute2.value), + and_(Attribute1.value == itemID, queryOr), + from_obj=[ + join(Attribute1, Attribute2, Attribute1.typeID == Attribute2.typeID) + ]) + + result = gamedata_session.execute(q).fetchall() + + return result diff --git a/eos/db/gamedata/unit.py b/eos/db/gamedata/unit.py index 5c6556ef1..3fd49dac7 100644 --- a/eos/db/gamedata/unit.py +++ b/eos/db/gamedata/unit.py @@ -29,5 +29,7 @@ groups_table = Table("dgmunits", gamedata_meta, Column("displayName", String)) mapper(Unit, groups_table, - properties={"ID": synonym("unitID"), - "name": synonym("unitName")}) + properties={ + "ID" : synonym("unitID"), + "name": synonym("unitName") + }) diff --git a/eos/db/migrations/upgrade1.py b/eos/db/migrations/upgrade1.py index 5eb3478c6..22292dc4f 100644 --- a/eos/db/migrations/upgrade1.py +++ b/eos/db/migrations/upgrade1.py @@ -14,32 +14,32 @@ Migration 1 import sqlalchemy CONVERSIONS = { - 6135: [ # Scoped Cargo Scanner + 6135 : [ # Scoped Cargo Scanner 6133, # Interior Type-E Cargo Identifier ], - 6527: [ # Compact Ship Scanner + 6527 : [ # Compact Ship Scanner 6525, # Ta3 Perfunctory Vessel Probe 6529, # Speculative Ship Identifier I 6531, # Practical Type-E Ship Probe ], - 6569: [ # Scoped Survey Scanner + 6569 : [ # Scoped Survey Scanner 6567, # ML-3 Amphilotite Mining Probe 6571, # Rock-Scanning Sensor Array I 6573, # 'Dactyl' Type-E Asteroid Analyzer ], - 509: [ # 'Basic' Capacitor Flux Coil + 509 : [ # 'Basic' Capacitor Flux Coil 8163, # Partial Power Plant Manager: Capacitor Flux 8165, # Alpha Reactor Control: Capacitor Flux 8167, # Type-E Power Core Modification: Capacitor Flux 8169, # Marked Generator Refitting: Capacitor Flux ], - 8135: [ # Restrained Capacitor Flux Coil + 8135 : [ # Restrained Capacitor Flux Coil 8131, # Local Power Plant Manager: Capacitor Flux I ], - 8133: [ # Compact Capacitor Flux Coil + 8133 : [ # Compact Capacitor Flux Coil 8137, # Mark I Generator Refitting: Capacitor Flux ], - 3469: [ # Basic Co-Processor + 3469 : [ # Basic Co-Processor 8744, # Nanoelectrical Co-Processor 8743, # Nanomechanical CPU Enhancer 8746, # Quantum Co-Processor @@ -47,17 +47,17 @@ CONVERSIONS = { 15425, # Naiyon's Modified Co-Processor (never existed but convert # anyway as some fits may include it) ], - 8748: [ # Upgraded Co-Processor + 8748 : [ # Upgraded Co-Processor 8747, # Nanomechanical CPU Enhancer I 8750, # Quantum Co-Processor I 8749, # Photonic CPU Enhancer I ], - 1351: [ # Basic Reactor Control Unit + 1351 : [ # Basic Reactor Control Unit 8251, # Partial Power Plant Manager: Reaction Control 8253, # Alpha Reactor Control: Reaction Control 8257, # Marked Generator Refitting: Reaction Control ], - 8263: [ # Compact Reactor Control Unit + 8263 : [ # Compact Reactor Control Unit 8259, # Local Power Plant Manager: Reaction Control I 8265, # Mark I Generator Refitting: Reaction Control 8261, # Beta Reactor Control: Reaction Control I @@ -69,15 +69,15 @@ CONVERSIONS = { 31936: [ # Navy Micro Auxiliary Power Core 16543, # Micro 'Vigor' Core Augmentation ], - 8089: [ # Compact Light Missile Launcher + 8089 : [ # Compact Light Missile Launcher 8093, # Prototype 'Arbalest' Light Missile Launcher ], - 8091: [ # Ample Light Missile Launcher + 8091 : [ # Ample Light Missile Launcher 7993, # Experimental TE-2100 Light Missile Launcher ], # Surface Cargo Scanner I was removed from game, however no mention of # replacement module in patch notes. Morphing it to meta 0 module to be safe - 442: [ # Cargo Scanner I + 442 : [ # Cargo Scanner I 6129, # Surface Cargo Scanner I ] } diff --git a/eos/db/migrations/upgrade11.py b/eos/db/migrations/upgrade11.py index 475537b01..2811cf5be 100644 --- a/eos/db/migrations/upgrade11.py +++ b/eos/db/migrations/upgrade11.py @@ -14,7 +14,7 @@ CONVERSIONS = { 22947: ( # 'Beatnik' Small Remote Armor Repairer 23414, # 'Brotherhood' Small Remote Armor Repairer ), - 8295: ( # Type-D Restrained Shield Flux Coil + 8295 : ( # Type-D Restrained Shield Flux Coil 8293, # Beta Reactor Control: Shield Flux I ), 16499: ( # Heavy Knave Scoped Energy Nosferatu @@ -29,13 +29,13 @@ CONVERSIONS = { 16447: ( # Medium Solace Scoped Remote Armor Repairer 16445, # Medium 'Arup' Remote Armor Repairer ), - 508: ( # 'Basic' Shield Flux Coil + 508 : ( # 'Basic' Shield Flux Coil 8325, # Alpha Reactor Shield Flux 8329, # Marked Generator Refitting: Shield Flux 8323, # Partial Power Plant Manager: Shield Flux 8327, # Type-E Power Core Modification: Shield Flux ), - 1419: ( # 'Basic' Shield Power Relay + 1419 : ( # 'Basic' Shield Power Relay 8341, # Alpha Reactor Shield Power Relay 8345, # Marked Generator Refitting: Shield Power Relay 8339, # Partial Power Plant Manager: Shield Power Relay @@ -47,57 +47,57 @@ CONVERSIONS = { 16505: ( # Medium Ghoul Compact Energy Nosferatu 16511, # Medium Diminishing Power System Drain I ), - 8297: ( # Mark I Compact Shield Flux Coil + 8297 : ( # Mark I Compact Shield Flux Coil 8291, # Local Power Plant Manager: Reaction Shield Flux I ), 16455: ( # Large Solace Scoped Remote Armor Repairer 16453, # Large 'Arup' Remote Armor Repairer ), - 6485: ( # M51 Benefactor Compact Shield Recharger + 6485 : ( # M51 Benefactor Compact Shield Recharger 6491, # Passive Barrier Compensator I 6489, # 'Benefactor' Ward Reconstructor 6487, # Supplemental Screen Generator I ), - 5137: ( # Small Knave Scoped Energy Nosferatu + 5137 : ( # Small Knave Scoped Energy Nosferatu 5135, # E5 Prototype Energy Vampire ), - 8579: ( # Medium Murky Compact Remote Shield Booster + 8579 : ( # Medium Murky Compact Remote Shield Booster 8581, # Medium 'Atonement' Remote Shield Booster ), - 8531: ( # Small Murky Compact Remote Shield Booster + 8531 : ( # Small Murky Compact Remote Shield Booster 8533, # Small 'Atonement' Remote Shield Booster ), 16497: ( # Heavy Ghoul Compact Energy Nosferatu 16503, # Heavy Diminishing Power System Drain I ), - 4477: ( # Small Gremlin Compact Energy Neutralizer + 4477 : ( # Small Gremlin Compact Energy Neutralizer 4475, # Small Unstable Power Fluctuator I ), - 8337: ( # Mark I Compact Shield Power Relay + 8337 : ( # Mark I Compact Shield Power Relay 8331, # Local Power Plant Manager: Reaction Shield Power Relay I ), 23416: ( # 'Peace' Large Remote Armor Repairer 22951, # 'Pacifier' Large Remote Armor Repairer ), - 5141: ( # Small Ghoul Compact Energy Nosferatu + 5141 : ( # Small Ghoul Compact Energy Nosferatu 5139, # Small Diminishing Power System Drain I ), - 4471: ( # Small Infectious Scoped Energy Neutralizer + 4471 : ( # Small Infectious Scoped Energy Neutralizer 4473, # Small Rudimentary Energy Destabilizer I ), 16469: ( # Medium Infectious Scoped Energy Neutralizer 16465, # Medium Rudimentary Energy Destabilizer I ), - 8335: ( # Type-D Restrained Shield Power Relay + 8335 : ( # Type-D Restrained Shield Power Relay 8333, # Beta Reactor Control: Shield Power Relay I ), - 405: ( # 'Micro' Remote Shield Booster + 405 : ( # 'Micro' Remote Shield Booster 8631, # Micro Asymmetric Remote Shield Booster 8627, # Micro Murky Remote Shield Booster 8629, # Micro 'Atonement' Remote Shield Booster 8633, # Micro S95a Remote Shield Booster ), - 8635: ( # Large Murky Compact Remote Shield Booster + 8635 : ( # Large Murky Compact Remote Shield Booster 8637, # Large 'Atonement' Remote Shield Booster ), 16507: ( # Medium Knave Scoped Energy Nosferatu diff --git a/eos/db/migrations/upgrade12.py b/eos/db/migrations/upgrade12.py index 6e3a1d73b..59d6d08d9 100644 --- a/eos/db/migrations/upgrade12.py +++ b/eos/db/migrations/upgrade12.py @@ -13,26 +13,26 @@ CONVERSIONS = { 16461, # Multiphasic Bolt Array I 16463, # 'Pandemonium' Ballistic Enhancement ), - 5281: ( # Coadjunct Scoped Remote Sensor Booster + 5281 : ( # Coadjunct Scoped Remote Sensor Booster 7218, # Piercing ECCM Emitter I ), - 5365: ( # Cetus Scoped Burst Jammer + 5365 : ( # Cetus Scoped Burst Jammer 5359, # 1Z-3 Subversive ECM Eruption ), - 1973: ( # Sensor Booster I + 1973 : ( # Sensor Booster I 1947, # ECCM - Radar I 2002, # ECCM - Ladar I 2003, # ECCM - Magnetometric I 2004, # ECCM - Gravimetric I 2005, # ECCM - Omni I ), - 1951: ( # 'Basic' Tracking Enhancer + 1951 : ( # 'Basic' Tracking Enhancer 6322, # Beta-Nought Tracking Mode 6323, # Azimuth Descalloping Tracking Enhancer 6324, # F-AQ Delay-Line Scan Tracking Subroutines 6321, # Beam Parallax Tracking Program ), - 521: ( # 'Basic' Damage Control + 521 : ( # 'Basic' Damage Control 5829, # GLFF Containment Field 5831, # Interior Force Field Array 5835, # F84 Local Damage System @@ -42,13 +42,13 @@ CONVERSIONS = { 22939, # 'Boss' Remote Sensor Booster 22941, # 'Entrepreneur' Remote Sensor Booster ), - 5443: ( # Faint Epsilon Scoped Warp Scrambler + 5443 : ( # Faint Epsilon Scoped Warp Scrambler 5441, # Fleeting Progressive Warp Scrambler I ), - 1963: ( # Remote Sensor Booster I + 1963 : ( # Remote Sensor Booster I 1959, # ECCM Projector I ), - 6325: ( # Fourier Compact Tracking Enhancer + 6325 : ( # Fourier Compact Tracking Enhancer 6326, # Sigma-Nought Tracking Mode I 6327, # Auto-Gain Control Tracking Enhancer I 6328, # F-aQ Phase Code Tracking Subroutines @@ -68,19 +68,19 @@ CONVERSIONS = { 22919: ( # 'Monopoly' Magnetic Field Stabilizer 22917, # 'Capitalist' Magnetic Field Stabilizer I ), - 5839: ( # IFFA Compact Damage Control + 5839 : ( # IFFA Compact Damage Control 5841, # Emergency Damage Control I 5843, # F85 Peripheral Damage System I 5837, # Pseudoelectron Containment Field I ), - 522: ( # 'Micro' Cap Battery + 522 : ( # 'Micro' Cap Battery 4747, # Micro Ld-Acid Capacitor Battery I 4751, # Micro Ohm Capacitor Reserve I 4745, # Micro F-4a Ld-Sulfate Capacitor Charge Unit 4749, # Micro Peroxide Capacitor Power Cell 3480, # Micro Capacitor Battery II ), - 518: ( # 'Basic' Gyrostabilizer + 518 : ( # 'Basic' Gyrostabilizer 5915, # Lateral Gyrostabilizer 5919, # F-M2 Weapon Inertial Suspensor 5913, # Hydraulic Stabilization Actuator @@ -89,19 +89,19 @@ CONVERSIONS = { 19931: ( # Compulsive Scoped Multispectral ECM 19933, # 'Hypnos' Multispectral ECM I ), - 5403: ( # Faint Scoped Warp Disruptor + 5403 : ( # Faint Scoped Warp Disruptor 5401, # Fleeting Warp Disruptor I ), 23902: ( # 'Trebuchet' Heat Sink I 23900, # 'Mangonel' Heat Sink I ), - 1893: ( # 'Basic' Heat Sink + 1893 : ( # 'Basic' Heat Sink 5845, # Heat Exhaust System 5856, # C3S Convection Thermal Radiator 5855, # 'Boreas' Coolant System 5854, # Stamped Heat Sink ), - 6160: ( # F-90 Compact Sensor Booster + 6160 : ( # F-90 Compact Sensor Booster 20214, # Extra Radar ECCM Scanning Array I 20220, # Extra Ladar ECCM Scanning Array I 20226, # Extra Gravimetric ECCM Scanning Array I @@ -123,40 +123,40 @@ CONVERSIONS = { 19952: ( # Umbra Scoped Radar ECM 9520, # 'Penumbra' White Noise ECM ), - 1952: ( # Sensor Booster II + 1952 : ( # Sensor Booster II 2258, # ECCM - Omni II 2259, # ECCM - Gravimetric II 2260, # ECCM - Ladar II 2261, # ECCM - Magnetometric II 2262, # ECCM - Radar II ), - 5282: ( # Linked Enduring Sensor Booster + 5282 : ( # Linked Enduring Sensor Booster 7219, # Scattering ECCM Projector I ), - 1986: ( # Signal Amplifier I + 1986 : ( # Signal Amplifier I 2579, # Gravimetric Backup Array I 2583, # Ladar Backup Array I 2587, # Magnetometric Backup Array I 2591, # Multi Sensor Backup Array I 4013, # RADAR Backup Array I ), - 4871: ( # Large Compact Pb-Acid Cap Battery + 4871 : ( # Large Compact Pb-Acid Cap Battery 4875, # Large Ohm Capacitor Reserve I 4869, # Large F-4a Ld-Sulfate Capacitor Charge Unit 4873, # Large Peroxide Capacitor Power Cell ), - 1964: ( # Remote Sensor Booster II + 1964 : ( # Remote Sensor Booster II 1960, # ECCM Projector II ), - 5933: ( # Counterbalanced Compact Gyrostabilizer + 5933 : ( # Counterbalanced Compact Gyrostabilizer 5931, # Cross-Lateral Gyrostabilizer I 5935, # F-M3 Munition Inertial Suspensor 5929, # Pneumatic Stabilization Actuator I ), - 4025: ( # X5 Enduring Stasis Webifier + 4025 : ( # X5 Enduring Stasis Webifier 4029, # 'Langour' Drive Disruptor I ), - 4027: ( # Fleeting Compact Stasis Webifier + 4027 : ( # Fleeting Compact Stasis Webifier 4031, # Patterned Stasis Web I ), 22937: ( # 'Enterprise' Remote Tracking Computer @@ -165,7 +165,7 @@ CONVERSIONS = { 22929: ( # 'Marketeer' Tracking Computer 22927, # 'Economist' Tracking Computer I ), - 1987: ( # Signal Amplifier II + 1987 : ( # Signal Amplifier II 2580, # Gravimetric Backup Array II 2584, # Ladar Backup Array II 2588, # Magnetometric Backup Array II @@ -175,13 +175,13 @@ CONVERSIONS = { 19939: ( # Enfeebling Scoped Ladar ECM 9522, # Faint Phase Inversion ECM I ), - 5340: ( # P-S Compact Remote Tracking Computer + 5340 : ( # P-S Compact Remote Tracking Computer 5341, # 'Prayer' Remote Tracking Computer ), 19814: ( # Phased Scoped Target Painter 19808, # Partial Weapon Navigation ), - 1949: ( # 'Basic' Signal Amplifier + 1949 : ( # 'Basic' Signal Amplifier 1946, # Basic RADAR Backup Array 1982, # Basic Ladar Backup Array 1983, # Basic Gravimetric Backup Array @@ -222,10 +222,10 @@ CONVERSIONS = { 23416: ( # 'Peace' Large Remote Armor Repairer None, # 'Pacifier' Large Remote Armor Repairer ), - 6176: ( # F-12 Enduring Tracking Computer + 6176 : ( # F-12 Enduring Tracking Computer 6174, # Monopulse Tracking Mechanism I ), - 6159: ( # Alumel-Wired Enduring Sensor Booster + 6159 : ( # Alumel-Wired Enduring Sensor Booster 7917, # Alumel Radar ECCM Sensor Array I 7918, # Alumel Ladar ECCM Sensor Array I 7922, # Alumel Gravimetric ECCM Sensor Array I @@ -247,7 +247,7 @@ CONVERSIONS = { 7914, # Prototype ECCM Magnetometric Sensor Cluster 6158, # Prototype Sensor Booster ), - 5849: ( # Extruded Compact Heat Sink + 5849 : ( # Extruded Compact Heat Sink 5846, # Thermal Exhaust System I 5858, # C4S Coiled Circuit Thermal Radiator 5857, # 'Skadi' Coolant System I @@ -263,15 +263,15 @@ CONVERSIONS = { 22945: ( # 'Executive' Remote Sensor Dampener 22943, # 'Broker' Remote Sensor Dampener I ), - 6173: ( # Optical Compact Tracking Computer + 6173 : ( # Optical Compact Tracking Computer 6175, # 'Orion' Tracking CPU I ), - 5279: ( # F-23 Compact Remote Sensor Booster + 5279 : ( # F-23 Compact Remote Sensor Booster 7217, # Spot Pulsing ECCM I 7220, # Phased Muon ECCM Caster I 5280, # Connected Remote Sensor Booster ), - 4787: ( # Small Compact Pb-Acid Cap Battery + 4787 : ( # Small Compact Pb-Acid Cap Battery 4791, # Small Ohm Capacitor Reserve I 4785, # Small F-4a Ld-Sulfate Capacitor Charge Unit 4789, # Small Peroxide Capacitor Power Cell @@ -279,7 +279,7 @@ CONVERSIONS = { 19946: ( # BZ-5 Scoped Gravimetric ECM 9519, # FZ-3 Subversive Spatial Destabilizer ECM ), - 6073: ( # Medium Compact Pb-Acid Cap Battery + 6073 : ( # Medium Compact Pb-Acid Cap Battery 6097, # Medium Ohm Capacitor Reserve I 6111, # Medium F-4a Ld-Sulfate Capacitor Charge Unit 6083, # Medium Peroxide Capacitor Power Cell @@ -287,7 +287,7 @@ CONVERSIONS = { 21484: ( # 'Full Duplex' Ballistic Control System 21482, # Ballistic 'Purge' Targeting System I ), - 6296: ( # F-89 Compact Signal Amplifier + 6296 : ( # F-89 Compact Signal Amplifier 6218, # Protected Gravimetric Backup Cluster I 6222, # Protected Ladar Backup Cluster I 6226, # Protected Magnetometric Backup Cluster I @@ -324,7 +324,7 @@ CONVERSIONS = { 6293, # Wavelength Signal Enhancer I 6295, # Type-D Attenuation Signal Augmentation ), - 5302: ( # Phased Muon Scoped Sensor Dampener + 5302 : ( # Phased Muon Scoped Sensor Dampener 5300, # Indirect Scanning Dampening Unit I ), } diff --git a/eos/db/migrations/upgrade22.py b/eos/db/migrations/upgrade22.py new file mode 100644 index 000000000..fb821c2d1 --- /dev/null +++ b/eos/db/migrations/upgrade22.py @@ -0,0 +1,45 @@ +""" +Migration 22 + +- Adds the created and modified fields to most tables +""" +import sqlalchemy + + +def upgrade(saveddata_engine): + + # 1 = created only + # 2 = created and modified + tables = { + "boosters": 2, + "cargo": 2, + "characters": 2, + "crest": 1, + "damagePatterns": 2, + "drones": 2, + "fighters": 2, + "fits": 2, + "projectedFits": 2, + "commandFits": 2, + "implants": 2, + "implantSets": 2, + "modules": 2, + "overrides": 2, + "characterSkills": 2, + "targetResists": 2 + } + + for table in tables.keys(): + + # midnight brain, there's probably a much more simple way to do this, but fuck it + if tables[table] > 0: + try: + saveddata_engine.execute("SELECT created FROM {0} LIMIT 1;".format(table)) + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE {} ADD COLUMN created DATETIME;".format(table)) + + if tables[table] > 1: + try: + saveddata_engine.execute("SELECT modified FROM {0} LIMIT 1;".format(table)) + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE {} ADD COLUMN modified DATETIME;".format(table)) diff --git a/eos/db/migrations/upgrade23.py b/eos/db/migrations/upgrade23.py new file mode 100644 index 000000000..d609e995c --- /dev/null +++ b/eos/db/migrations/upgrade23.py @@ -0,0 +1,13 @@ +""" +Migration 23 + +- Adds a sec status field to the character table +""" +import sqlalchemy + + +def upgrade(saveddata_engine): + try: + saveddata_engine.execute("SELECT secStatus FROM characters LIMIT 1") + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE characters ADD COLUMN secStatus FLOAT;") diff --git a/eos/db/migrations/upgrade24.py b/eos/db/migrations/upgrade24.py new file mode 100644 index 000000000..93e52b61c --- /dev/null +++ b/eos/db/migrations/upgrade24.py @@ -0,0 +1,14 @@ +""" +Migration 24 + +- Adds a boolean value to fit to signify if fit should ignore restrictions +""" +import sqlalchemy + + +def upgrade(saveddata_engine): + try: + saveddata_engine.execute("SELECT ignoreRestrictions FROM fits LIMIT 1") + except sqlalchemy.exc.DatabaseError: + saveddata_engine.execute("ALTER TABLE fits ADD COLUMN ignoreRestrictions BOOLEAN") + saveddata_engine.execute("UPDATE fits SET ignoreRestrictions = 0") diff --git a/eos/db/migrations/upgrade4.py b/eos/db/migrations/upgrade4.py index f8c670684..d1e46d10a 100644 --- a/eos/db/migrations/upgrade4.py +++ b/eos/db/migrations/upgrade4.py @@ -11,64 +11,64 @@ Migration 4 """ CONVERSIONS = { - 506: ( # 'Basic' Capacitor Power Relay + 506 : ( # 'Basic' Capacitor Power Relay 8205, # Alpha Reactor Control: Capacitor Power Relay 8209, # Marked Generator Refitting: Capacitor Power Relay 8203, # Partial Power Plant Manager: Capacity Power Relay 8207, # Type-E Power Core Modification: Capacitor Power Relay ), - 8177: ( # Mark I Compact Capacitor Power Relay + 8177 : ( # Mark I Compact Capacitor Power Relay 8173, # Beta Reactor Control: Capacitor Power Relay I ), - 8175: ( # Type-D Restrained Capacitor Power Relay + 8175 : ( # Type-D Restrained Capacitor Power Relay 8171, # Local Power Plant Manager: Capacity Power Relay I ), - 421: ( # 'Basic' Capacitor Recharger + 421 : ( # 'Basic' Capacitor Recharger 4425, # AGM Capacitor Charge Array, 4421, # F-a10 Buffer Capacitor Regenerator 4423, # Industrial Capacitor Recharger 4427, # Secondary Parallel Link-Capacitor ), - 4435: ( # Eutectic Compact Cap Recharger + 4435 : ( # Eutectic Compact Cap Recharger 4433, # Barton Reactor Capacitor Recharger I 4431, # F-b10 Nominal Capacitor Regenerator 4437, # Fixed Parallel Link-Capacitor I ), - 1315: ( # 'Basic' Expanded Cargohold + 1315 : ( # 'Basic' Expanded Cargohold 5483, # Alpha Hull Mod Expanded Cargo 5479, # Marked Modified SS Expanded Cargo 5481, # Partial Hull Conversion Expanded Cargo 5485, # Type-E Altered SS Expanded Cargo ), - 5493: ( # Type-D Restrained Expanded Cargo + 5493 : ( # Type-D Restrained Expanded Cargo 5491, # Beta Hull Mod Expanded Cargo 5489, # Local Hull Conversion Expanded Cargo I 5487, # Mark I Modified SS Expanded Cargo ), - 1401: ( # 'Basic' Inertial Stabilizers + 1401 : ( # 'Basic' Inertial Stabilizers 5523, # Alpha Hull Mod Inertial Stabilizers 5521, # Partial Hull Conversion Inertial Stabilizers 5525, # Type-E Altered SS Inertial Stabilizers ), - 5533: ( # Type-D Restrained Inertial Stabilizers + 5533 : ( # Type-D Restrained Inertial Stabilizers 5531, # Beta Hull Mod Inertial Stabilizers 5529, # Local Hull Conversion Inertial Stabilizers I 5527, # Mark I Modified SS Inertial Stabilizers 5519, # Marked Modified SS Inertial Stabilizers ), - 5239: ( # EP-S Gaussian Scoped Mining Laser + 5239 : ( # EP-S Gaussian Scoped Mining Laser 5241, # Dual Diode Mining Laser I ), - 5233: ( # Single Diode Basic Mining Laser + 5233 : ( # Single Diode Basic Mining Laser 5231, # EP-R Argon Ion Basic Excavation Pulse 5237, # Rubin Basic Particle Bore Stream 5235, # Xenon Basic Drilling Beam ), - 5245: ( # Particle Bore Compact Mining Laser + 5245 : ( # Particle Bore Compact Mining Laser 5243, # XeCl Drilling Beam I ), @@ -79,53 +79,53 @@ CONVERSIONS = { 22609, # Erin Mining Laser Upgrade ), - 1242: ( # 'Basic' Nanofiber Internal Structure + 1242 : ( # 'Basic' Nanofiber Internal Structure 5591, # Alpha Hull Mod Nanofiber Structure 5595, # Marked Modified SS Nanofiber Structure 5559, # Partial Hull Conversion Nanofiber Structure 5593, # Type-E Altered SS Nanofiber Structure ), - 5599: ( # Type-D Restrained Nanofiber Structure + 5599 : ( # Type-D Restrained Nanofiber Structure 5597, # Beta Hull Mod Nanofiber Structure 5561, # Local Hull Conversion Nanofiber Structure I 5601, # Mark I Modified SS Nanofiber Structure ), - 1192: ( # 'Basic' Overdrive Injector System + 1192 : ( # 'Basic' Overdrive Injector System 5613, # Alpha Hull Mod Overdrive Injector 5617, # Marked Modified SS Overdrive Injector 5611, # Partial Hull Conversion Overdrive Injector 5615, # Type-E Altered SS Overdrive Injector ), - 5631: ( # Type-D Restrained Overdrive Injector + 5631 : ( # Type-D Restrained Overdrive Injector 5629, # Beta Hull Mod Overdrive Injector 5627, # Local Hull Conversion Overdrive Injector I 5633, # Mark I Modified SS Overdrive Injector ), - 1537: ( # 'Basic' Power Diagnostic System + 1537 : ( # 'Basic' Power Diagnostic System 8213, # Alpha Reactor Control: Diagnostic System 8217, # Marked Generator Refitting: Diagnostic System 8211, # Partial Power Plant Manager: Diagnostic System 8215, # Type-E Power Core Modification: Diagnostic System 8255, # Type-E Power Core Modification: Reaction Control ), - 8225: ( # Mark I Compact Power Diagnostic System + 8225 : ( # Mark I Compact Power Diagnostic System 8221, # Beta Reactor Control: Diagnostic System I 8219, # Local Power Plant Manager: Diagnostic System I 8223, # Type-D Power Core Modification: Diagnostic System ), - 1240: ( # 'Basic' Reinforced Bulkheads + 1240 : ( # 'Basic' Reinforced Bulkheads 5677, # Alpha Hull Mod Reinforced Bulkheads 5681, # Marked Modified SS Reinforced Bulkheads 5675, # Partial Hull Conversion Reinforced Bulkheads 5679, # Type-E Altered SS Reinforced Bulkheads ), - 5649: ( # Mark I Compact Reinforced Bulkheads + 5649 : ( # Mark I Compact Reinforced Bulkheads 5645, # Beta Hull Mod Reinforced Bulkheads ), - 5647: ( # Type-D Restrained Reinforced Bulkheads + 5647 : ( # Type-D Restrained Reinforced Bulkheads 5643, # Local Hull Conversion Reinforced Bulkheads I ), } diff --git a/eos/db/migrations/upgrade8.py b/eos/db/migrations/upgrade8.py index 19185443b..0051034ec 100644 --- a/eos/db/migrations/upgrade8.py +++ b/eos/db/migrations/upgrade8.py @@ -8,16 +8,16 @@ Migration 8 """ CONVERSIONS = { - 8529: ( # Large F-S9 Regolith Compact Shield Extender + 8529 : ( # Large F-S9 Regolith Compact Shield Extender 8409, # Large Subordinate Screen Stabilizer I ), - 8419: ( # Large Azeotropic Restrained Shield Extender + 8419 : ( # Large Azeotropic Restrained Shield Extender 8489, # Large Supplemental Barrier Emitter I ), - 8517: ( # Medium F-S9 Regolith Compact Shield Extender + 8517 : ( # Medium F-S9 Regolith Compact Shield Extender 8397, # Medium Subordinate Screen Stabilizer I ), - 8433: ( # Medium Azeotropic Restrained Shield Extender + 8433 : ( # Medium Azeotropic Restrained Shield Extender 8477, # Medium Supplemental Barrier Emitter I ), 20627: ( # Small 'Trapper' Shield Extender @@ -28,10 +28,10 @@ CONVERSIONS = { 8387, # Micro Subordinate Screen Stabilizer I 8465, # Micro Supplemental Barrier Emitter I ), - 8521: ( # Small F-S9 Regolith Compact Shield Extender + 8521 : ( # Small F-S9 Regolith Compact Shield Extender 8401, # Small Subordinate Screen Stabilizer I ), - 8427: ( # Small Azeotropic Restrained Shield Extender + 8427 : ( # Small Azeotropic Restrained Shield Extender 8481, # Small Supplemental Barrier Emitter I ), 11343: ( # 100mm Crystalline Carbonide Restrained Plates diff --git a/eos/db/migrations/upgrade9.py b/eos/db/migrations/upgrade9.py index a2f5b6148..af23ff745 100644 --- a/eos/db/migrations/upgrade9.py +++ b/eos/db/migrations/upgrade9.py @@ -20,6 +20,6 @@ CREATE TABLE boostersTemp ( def upgrade(saveddata_engine): saveddata_engine.execute(tmpTable) saveddata_engine.execute( - "INSERT INTO boostersTemp (ID, itemID, fitID, active) SELECT ID, itemID, fitID, active FROM boosters") + "INSERT INTO boostersTemp (ID, itemID, fitID, active) SELECT ID, itemID, fitID, active FROM boosters") saveddata_engine.execute("DROP TABLE boosters") saveddata_engine.execute("ALTER TABLE boostersTemp RENAME TO boosters") diff --git a/eos/db/saveddata/booster.py b/eos/db/saveddata/booster.py index 11ef16506..e40762cd9 100644 --- a/eos/db/saveddata/booster.py +++ b/eos/db/saveddata/booster.py @@ -17,18 +17,21 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, ForeignKey, Integer, Boolean +from sqlalchemy import Table, Column, ForeignKey, Integer, Boolean, DateTime from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import mapper, relation +import datetime from eos.db import saveddata_meta from eos.saveddata.booster import Booster boosters_table = Table("boosters", saveddata_meta, - Column("ID", Integer, primary_key=True), - Column("itemID", Integer), - Column("fitID", Integer, ForeignKey("fits.ID"), nullable=False), - Column("active", Boolean), + Column("ID", Integer, primary_key=True), + Column("itemID", Integer), + Column("fitID", Integer, ForeignKey("fits.ID"), nullable=False), + Column("active", Boolean), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), ) # Legacy booster side effect code, should disable but a mapper relies on it. diff --git a/eos/db/saveddata/cargo.py b/eos/db/saveddata/cargo.py index a0222ac3b..b9b23adaf 100644 --- a/eos/db/saveddata/cargo.py +++ b/eos/db/saveddata/cargo.py @@ -17,16 +17,25 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey -from sqlalchemy.orm import mapper +from sqlalchemy import Table, Column, Integer, ForeignKey, DateTime +from sqlalchemy.orm import mapper, relation +import datetime from eos.db import saveddata_meta from eos.saveddata.cargo import Cargo +from eos.saveddata.fit import Fit cargo_table = Table("cargo", saveddata_meta, Column("ID", Integer, primary_key=True), Column("fitID", Integer, ForeignKey("fits.ID"), nullable=False, index=True), Column("itemID", Integer, nullable=False), - Column("amount", Integer, nullable=False)) + Column("amount", Integer, nullable=False), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), + ) -mapper(Cargo, cargo_table) +mapper(Cargo, cargo_table, + properties={ + "owner": relation(Fit) + } +) diff --git a/eos/db/saveddata/character.py b/eos/db/saveddata/character.py index aaa8a3829..c87817541 100644 --- a/eos/db/saveddata/character.py +++ b/eos/db/saveddata/character.py @@ -17,8 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey, String +from sqlalchemy import Table, Column, Integer, ForeignKey, String, DateTime, Float from sqlalchemy.orm import relation, mapper +import datetime from eos.db import saveddata_meta from eos.db.saveddata.implant import charImplants_table @@ -36,27 +37,31 @@ characters_table = Table("characters", saveddata_meta, Column("chars", String, nullable=True), Column("defaultLevel", Integer, nullable=True), Column("alphaCloneID", Integer, nullable=True), - Column("ownerID", ForeignKey("users.ID"), nullable=True)) + Column("ownerID", ForeignKey("users.ID"), nullable=True), + Column("secStatus", Float, nullable=True, default=0.0), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now)) mapper(Character, characters_table, properties={ "_Character__alphaCloneID": characters_table.c.alphaCloneID, - "savedName": characters_table.c.name, - "_Character__owner": relation( - User, - backref="characters"), - "_Character__skills": relation( - Skill, - backref="character", - cascade="all,delete-orphan"), - "_Character__implants": relation( - Implant, - collection_class=HandledImplantBoosterList, - cascade='all,delete-orphan', - backref='character', - single_parent=True, - primaryjoin=charImplants_table.c.charID == characters_table.c.ID, - secondaryjoin=charImplants_table.c.implantID == Implant.ID, - secondary=charImplants_table), + "savedName" : characters_table.c.name, + "_Character__secStatus": characters_table.c.secStatus, + "_Character__owner" : relation( + User, + backref="characters"), + "_Character__skills" : relation( + Skill, + backref="character", + cascade="all,delete-orphan"), + "_Character__implants" : relation( + Implant, + collection_class=HandledImplantBoosterList, + cascade='all,delete-orphan', + backref='character', + single_parent=True, + primaryjoin=charImplants_table.c.charID == characters_table.c.ID, + secondaryjoin=charImplants_table.c.implantID == Implant.ID, + secondary=charImplants_table), } ) diff --git a/eos/db/saveddata/crest.py b/eos/db/saveddata/crest.py index c5cbcbb05..28f77a983 100644 --- a/eos/db/saveddata/crest.py +++ b/eos/db/saveddata/crest.py @@ -17,8 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, String +from sqlalchemy import Table, Column, Integer, String, DateTime from sqlalchemy.orm import mapper +import datetime from eos.db import saveddata_meta from eos.saveddata.crestchar import CrestChar @@ -26,6 +27,8 @@ from eos.saveddata.crestchar import CrestChar crest_table = Table("crest", saveddata_meta, Column("ID", Integer, primary_key=True), Column("name", String, nullable=False, unique=True), - Column("refresh_token", String, nullable=False)) + Column("refresh_token", String, nullable=False), + # These records aren't updated. Instead, they are dropped and created, hence we don't have a modified field + Column("created", DateTime, nullable=True, default=datetime.datetime.now)) mapper(CrestChar, crest_table) diff --git a/eos/db/saveddata/damagePattern.py b/eos/db/saveddata/damagePattern.py index d31c66ea6..8a2536703 100644 --- a/eos/db/saveddata/damagePattern.py +++ b/eos/db/saveddata/damagePattern.py @@ -17,8 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey, String +from sqlalchemy import Table, Column, Integer, ForeignKey, String, DateTime from sqlalchemy.orm import mapper +import datetime from eos.db import saveddata_meta from eos.saveddata.damagePattern import DamagePattern @@ -30,6 +31,9 @@ damagePatterns_table = Table("damagePatterns", saveddata_meta, Column("thermalAmount", Integer), Column("kineticAmount", Integer), Column("explosiveAmount", Integer), - Column("ownerID", ForeignKey("users.ID"), nullable=True)) + Column("ownerID", ForeignKey("users.ID"), nullable=True), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) + ) mapper(DamagePattern, damagePatterns_table) diff --git a/eos/db/saveddata/databaseRepair.py b/eos/db/saveddata/databaseRepair.py index d16c412b0..e40a022f0 100644 --- a/eos/db/saveddata/databaseRepair.py +++ b/eos/db/saveddata/databaseRepair.py @@ -168,7 +168,7 @@ class DatabaseCleanup(object): for table in ['drones', 'cargo', 'fighters']: pyfalog.debug("Running database cleanup for orphaned {0} items.", table) query = "SELECT COUNT(*) AS num FROM {} WHERE itemID IS NULL OR itemID = '' or itemID = '0' or fitID IS NULL OR fitID = '' or fitID = '0'".format( - table) + table) results = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query) if results is None: @@ -178,14 +178,14 @@ class DatabaseCleanup(object): if row and row['num']: query = "DELETE FROM {} WHERE itemID IS NULL OR itemID = '' or itemID = '0' or fitID IS NULL OR fitID = '' or fitID = '0'".format( - table) + table) delete = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query) pyfalog.error("Database corruption found. Cleaning up {0} records.", delete.rowcount) for table in ['modules']: pyfalog.debug("Running database cleanup for orphaned {0} items.", table) query = "SELECT COUNT(*) AS num FROM {} WHERE itemID = '0' or fitID IS NULL OR fitID = '' or fitID = '0'".format( - table) + table) results = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query) if results is None: @@ -216,7 +216,7 @@ class DatabaseCleanup(object): if row and row['num']: query = "UPDATE '{0}' SET '{1}Amount' = '0' WHERE {1}Amount IS NULL OR {1}Amount = ''".format(profileType, - damageType) + damageType) delete = DatabaseCleanup.ExecuteSQLQuery(saveddata_engine, query) pyfalog.error("Database corruption found. Cleaning up {0} records.", delete.rowcount) diff --git a/eos/db/saveddata/drone.py b/eos/db/saveddata/drone.py index 6a5ffa8bb..b4c0cefb9 100644 --- a/eos/db/saveddata/drone.py +++ b/eos/db/saveddata/drone.py @@ -17,11 +17,13 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean -from sqlalchemy.orm import mapper +from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime +from sqlalchemy.orm import mapper, relation +import datetime from eos.db import saveddata_meta from eos.saveddata.drone import Drone +from eos.saveddata.fit import Fit drones_table = Table("drones", saveddata_meta, Column("groupID", Integer, primary_key=True), @@ -29,6 +31,13 @@ drones_table = Table("drones", saveddata_meta, Column("itemID", Integer, nullable=False), Column("amount", Integer, nullable=False), Column("amountActive", Integer, nullable=False), - Column("projected", Boolean, default=False)) + Column("projected", Boolean, default=False), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) + ) -mapper(Drone, drones_table) +mapper(Drone, drones_table, + properties={ + "owner": relation(Fit) + } +) diff --git a/eos/db/saveddata/fighter.py b/eos/db/saveddata/fighter.py index bd7d8fd54..bb1ed133f 100644 --- a/eos/db/saveddata/fighter.py +++ b/eos/db/saveddata/fighter.py @@ -17,8 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean +from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime from sqlalchemy.orm import mapper, relation +import datetime from eos.db import saveddata_meta from eos.saveddata.fighterAbility import FighterAbility @@ -31,7 +32,10 @@ fighters_table = Table("fighters", saveddata_meta, Column("itemID", Integer, nullable=False), Column("active", Boolean, nullable=True), Column("amount", Integer, nullable=False), - Column("projected", Boolean, default=False)) + Column("projected", Boolean, default=False), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) + ) fighter_abilities_table = Table("fightersAbilities", saveddata_meta, Column("groupID", Integer, ForeignKey("fighters.groupID"), primary_key=True, @@ -41,11 +45,11 @@ fighter_abilities_table = Table("fightersAbilities", saveddata_meta, mapper(Fighter, fighters_table, properties={ - "owner": relation(Fit), + "owner" : relation(Fit), "_Fighter__abilities": relation( - FighterAbility, - backref="fighter", - cascade='all, delete, delete-orphan'), + FighterAbility, + backref="fighter", + cascade='all, delete, delete-orphan'), }) mapper(FighterAbility, fighter_abilities_table) diff --git a/eos/db/saveddata/fit.py b/eos/db/saveddata/fit.py index 8cbdb4d6f..07375dcaf 100644 --- a/eos/db/saveddata/fit.py +++ b/eos/db/saveddata/fit.py @@ -21,7 +21,8 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.sql import and_ from sqlalchemy.orm import relation, reconstructor, mapper, relationship -from sqlalchemy import ForeignKey, Column, Integer, String, Table, Boolean +from sqlalchemy import ForeignKey, Column, Integer, String, Table, Boolean, DateTime +import datetime from eos.db import saveddata_meta from eos.db import saveddata_session @@ -57,6 +58,9 @@ fits_table = Table("fits", saveddata_meta, Column("modeID", Integer, nullable=True), Column("implantLocation", Integer, nullable=False, default=ImplantLocation.FIT), Column("notes", String, nullable=True), + Column("ignoreRestrictions", Boolean, default=0), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, default=datetime.datetime.now, onupdate=datetime.datetime.now) ) projectedFits_table = Table("projectedFits", saveddata_meta, @@ -64,12 +68,16 @@ projectedFits_table = Table("projectedFits", saveddata_meta, Column("victimID", ForeignKey("fits.ID"), primary_key=True), Column("amount", Integer, nullable=False, default=1), Column("active", Boolean, nullable=False, default=1), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) commandFits_table = Table("commandFits", saveddata_meta, Column("boosterID", ForeignKey("fits.ID"), primary_key=True), Column("boostedID", ForeignKey("fits.ID"), primary_key=True), - Column("active", Boolean, nullable=False, default=1) + Column("active", Boolean, nullable=False, default=1), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) @@ -100,7 +108,7 @@ class ProjectedFit(object): def __repr__(self): return "ProjectedFit(sourceID={}, victimID={}, amount={}, active={}) at {}".format( - self.sourceID, self.victimID, self.amount, self.active, hex(id(self)) + self.sourceID, self.victimID, self.amount, self.active, hex(id(self)) ) @@ -120,120 +128,131 @@ class CommandFit(object): def __repr__(self): return "CommandFit(boosterID={}, boostedID={}, active={}) at {}".format( - self.boosterID, self.boostedID, self.active, hex(id(self)) + self.boosterID, self.boostedID, self.active, hex(id(self)) ) es_Fit._Fit__projectedFits = association_proxy( - "victimOf", # look at the victimOf association... - "source_fit", # .. and return the source fits - creator=lambda sourceID, source_fit: ProjectedFit(sourceID, source_fit) + "victimOf", # look at the victimOf association... + "source_fit", # .. and return the source fits + creator=lambda sourceID, source_fit: ProjectedFit(sourceID, source_fit) ) es_Fit._Fit__commandFits = association_proxy( - "boostedOf", # look at the boostedOf association... - "booster_fit", # .. and return the booster fit - creator=lambda boosterID, booster_fit: CommandFit(boosterID, booster_fit) + "boostedOf", # look at the boostedOf association... + "booster_fit", # .. and return the booster fit + creator=lambda boosterID, booster_fit: CommandFit(boosterID, booster_fit) ) + + +# These relationships are broken out so that we can easily access it in the events stuff +# We sometimes don't want particular relationships to cause a fit modified update (eg: projecting +# a fit onto another would 'modify' both fits unless the following relationship is ignored) +projectedFitSourceRel = relationship( + ProjectedFit, + primaryjoin=projectedFits_table.c.sourceID == fits_table.c.ID, + backref='source_fit', + collection_class=attribute_mapped_collection('victimID'), + cascade='all, delete, delete-orphan') + + +boostedOntoRel = relationship( + CommandFit, + primaryjoin=commandFits_table.c.boosterID == fits_table.c.ID, + backref='booster_fit', + collection_class=attribute_mapped_collection('boostedID'), + cascade='all, delete, delete-orphan') + mapper(es_Fit, fits_table, properties={ "_Fit__modules": relation( - Module, - collection_class=HandledModuleList, - primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False), # noqa - order_by=modules_table.c.position, - cascade='all, delete, delete-orphan'), + Module, + collection_class=HandledModuleList, + primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False), # noqa + order_by=modules_table.c.position, + cascade='all, delete, delete-orphan'), "_Fit__projectedModules": relation( - Module, - collection_class=HandledProjectedModList, - cascade='all, delete, delete-orphan', - single_parent=True, - primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)), # noqa + Module, + collection_class=HandledProjectedModList, + cascade='all, delete, delete-orphan', + single_parent=True, + primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)), # noqa "owner": relation( - User, - backref="fits"), + User, + backref="fits"), "itemID": fits_table.c.shipID, "shipID": fits_table.c.shipID, "_Fit__boosters": relation( - Booster, - collection_class=HandledImplantBoosterList, - cascade='all, delete, delete-orphan', - single_parent=True), + Booster, + collection_class=HandledImplantBoosterList, + cascade='all, delete, delete-orphan', + backref='owner', + single_parent=True), "_Fit__drones": relation( - Drone, - collection_class=HandledDroneCargoList, - cascade='all, delete, delete-orphan', - single_parent=True, - primaryjoin=and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)), # noqa + Drone, + collection_class=HandledDroneCargoList, + cascade='all, delete, delete-orphan', + single_parent=True, + primaryjoin=and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)), # noqa "_Fit__fighters": relation( - Fighter, - collection_class=HandledDroneCargoList, - cascade='all, delete, delete-orphan', - single_parent=True, - primaryjoin=and_(fighters_table.c.fitID == fits_table.c.ID, fighters_table.c.projected == False)), # noqa + Fighter, + collection_class=HandledDroneCargoList, + cascade='all, delete, delete-orphan', + single_parent=True, + primaryjoin=and_(fighters_table.c.fitID == fits_table.c.ID, fighters_table.c.projected == False)), # noqa "_Fit__cargo": relation( - Cargo, - collection_class=HandledDroneCargoList, - cascade='all, delete, delete-orphan', - single_parent=True, - primaryjoin=and_(cargo_table.c.fitID == fits_table.c.ID)), + Cargo, + collection_class=HandledDroneCargoList, + cascade='all, delete, delete-orphan', + single_parent=True, + primaryjoin=and_(cargo_table.c.fitID == fits_table.c.ID)), "_Fit__projectedDrones": relation( - Drone, - collection_class=HandledProjectedDroneList, - cascade='all, delete, delete-orphan', - single_parent=True, - primaryjoin=and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == True)), # noqa + Drone, + collection_class=HandledProjectedDroneList, + cascade='all, delete, delete-orphan', + single_parent=True, + primaryjoin=and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == True)), # noqa "_Fit__projectedFighters": relation( - Fighter, - collection_class=HandledProjectedDroneList, - cascade='all, delete, delete-orphan', - single_parent=True, - primaryjoin=and_(fighters_table.c.fitID == fits_table.c.ID, fighters_table.c.projected == True)), # noqa + Fighter, + collection_class=HandledProjectedDroneList, + cascade='all, delete, delete-orphan', + single_parent=True, + primaryjoin=and_(fighters_table.c.fitID == fits_table.c.ID, fighters_table.c.projected == True)), # noqa "_Fit__implants": relation( - Implant, - collection_class=HandledImplantBoosterList, - cascade='all, delete, delete-orphan', - backref='fit', - single_parent=True, - primaryjoin=fitImplants_table.c.fitID == fits_table.c.ID, - secondaryjoin=fitImplants_table.c.implantID == Implant.ID, - secondary=fitImplants_table), + Implant, + collection_class=HandledImplantBoosterList, + cascade='all, delete, delete-orphan', + backref='owner', + single_parent=True, + primaryjoin=fitImplants_table.c.fitID == fits_table.c.ID, + secondaryjoin=fitImplants_table.c.implantID == Implant.ID, + secondary=fitImplants_table), "_Fit__character": relation( - Character, - backref="fits"), + Character, + backref="fits"), "_Fit__damagePattern": relation(DamagePattern), "_Fit__targetResists": relation(TargetResists), - "projectedOnto": relationship( - ProjectedFit, - primaryjoin=projectedFits_table.c.sourceID == fits_table.c.ID, - backref='source_fit', - collection_class=attribute_mapped_collection('victimID'), - cascade='all, delete, delete-orphan'), + "projectedOnto": projectedFitSourceRel, "victimOf": relationship( - ProjectedFit, - primaryjoin=fits_table.c.ID == projectedFits_table.c.victimID, - backref='victim_fit', - collection_class=attribute_mapped_collection('sourceID'), - cascade='all, delete, delete-orphan'), - "boostedOnto": relationship( - CommandFit, - primaryjoin=commandFits_table.c.boosterID == fits_table.c.ID, - backref='booster_fit', - collection_class=attribute_mapped_collection('boostedID'), - cascade='all, delete, delete-orphan'), + ProjectedFit, + primaryjoin=fits_table.c.ID == projectedFits_table.c.victimID, + backref='victim_fit', + collection_class=attribute_mapped_collection('sourceID'), + cascade='all, delete, delete-orphan'), + "boostedOnto": boostedOntoRel, "boostedOf": relationship( - CommandFit, - primaryjoin=fits_table.c.ID == commandFits_table.c.boostedID, - backref='boosted_fit', - collection_class=attribute_mapped_collection('boosterID'), - cascade='all, delete, delete-orphan'), + CommandFit, + primaryjoin=fits_table.c.ID == commandFits_table.c.boostedID, + backref='boosted_fit', + collection_class=attribute_mapped_collection('boosterID'), + cascade='all, delete, delete-orphan'), } - ) +) mapper(ProjectedFit, projectedFits_table, - properties={ - "_ProjectedFit__amount": projectedFits_table.c.amount, - } - ) + properties={ + "_ProjectedFit__amount": projectedFits_table.c.amount, + } +) mapper(CommandFit, commandFits_table) diff --git a/eos/db/saveddata/implant.py b/eos/db/saveddata/implant.py index c5bfc4cc5..edf9aac12 100644 --- a/eos/db/saveddata/implant.py +++ b/eos/db/saveddata/implant.py @@ -17,8 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean +from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, DateTime from sqlalchemy.orm import mapper +import datetime from eos.db import saveddata_meta from eos.saveddata.implant import Implant @@ -26,7 +27,10 @@ from eos.saveddata.implant import Implant implants_table = Table("implants", saveddata_meta, Column("ID", Integer, primary_key=True), Column("itemID", Integer), - Column("active", Boolean)) + Column("active", Boolean), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) + ) fitImplants_table = Table("fitImplants", saveddata_meta, Column("fitID", ForeignKey("fits.ID"), index=True), diff --git a/eos/db/saveddata/implantSet.py b/eos/db/saveddata/implantSet.py index 955accc07..369f04760 100644 --- a/eos/db/saveddata/implantSet.py +++ b/eos/db/saveddata/implantSet.py @@ -17,8 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, String +from sqlalchemy import Table, Column, Integer, String, DateTime from sqlalchemy.orm import relation, mapper +import datetime from eos.db import saveddata_meta from eos.db.saveddata.implant import implantsSetMap_table @@ -29,18 +30,20 @@ from eos.saveddata.implantSet import ImplantSet implant_set_table = Table("implantSets", saveddata_meta, Column("ID", Integer, primary_key=True), Column("name", String, nullable=False), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) ) mapper(ImplantSet, implant_set_table, properties={ "_ImplantSet__implants": relation( - Implant, - collection_class=HandledImplantBoosterList, - cascade='all, delete, delete-orphan', - backref='set', - single_parent=True, - primaryjoin=implantsSetMap_table.c.setID == implant_set_table.c.ID, - secondaryjoin=implantsSetMap_table.c.implantID == Implant.ID, - secondary=implantsSetMap_table), + Implant, + collection_class=HandledImplantBoosterList, + cascade='all, delete, delete-orphan', + backref='set', + single_parent=True, + primaryjoin=implantsSetMap_table.c.setID == implant_set_table.c.ID, + secondaryjoin=implantsSetMap_table.c.implantID == Implant.ID, + secondary=implantsSetMap_table), } ) diff --git a/eos/db/saveddata/module.py b/eos/db/saveddata/module.py index c5533d4ed..149f4f73c 100644 --- a/eos/db/saveddata/module.py +++ b/eos/db/saveddata/module.py @@ -17,8 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey, CheckConstraint, Boolean +from sqlalchemy import Table, Column, Integer, ForeignKey, CheckConstraint, Boolean, DateTime from sqlalchemy.orm import relation, mapper +import datetime from eos.db import saveddata_meta from eos.saveddata.module import Module @@ -33,6 +34,8 @@ modules_table = Table("modules", saveddata_meta, Column("state", Integer, CheckConstraint("state >= -1"), CheckConstraint("state <= 2")), Column("projected", Boolean, default=False, nullable=False), Column("position", Integer), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now), CheckConstraint('("dummySlot" = NULL OR "itemID" = NULL) AND "dummySlot" != "itemID"')) mapper(Module, modules_table, diff --git a/eos/db/saveddata/override.py b/eos/db/saveddata/override.py index 2ca622966..aa99c9763 100644 --- a/eos/db/saveddata/override.py +++ b/eos/db/saveddata/override.py @@ -17,8 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, Float +from sqlalchemy import Table, Column, Integer, Float, DateTime from sqlalchemy.orm import mapper +import datetime from eos.db import saveddata_meta from eos.saveddata.override import Override @@ -26,6 +27,9 @@ from eos.saveddata.override import Override overrides_table = Table("overrides", saveddata_meta, Column("itemID", Integer, primary_key=True, index=True), Column("attrID", Integer, primary_key=True, index=True), - Column("value", Float, nullable=False)) + Column("value", Float, nullable=False), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) + ) mapper(Override, overrides_table) diff --git a/eos/db/saveddata/queries.py b/eos/db/saveddata/queries.py index 16f6a3bf9..4817b89e1 100644 --- a/eos/db/saveddata/queries.py +++ b/eos/db/saveddata/queries.py @@ -18,6 +18,7 @@ # =============================================================================== from sqlalchemy.sql import and_ +from sqlalchemy import desc, select from eos.db import saveddata_session, sd_lock from eos.db.saveddata.fit import projectedFits_table @@ -30,6 +31,7 @@ from eos.saveddata.targetResists import TargetResists from eos.saveddata.character import Character from eos.saveddata.implantSet import ImplantSet from eos.saveddata.fit import Fit +from eos.saveddata.module import Module from eos.saveddata.miscData import MiscData from eos.saveddata.override import Override @@ -175,7 +177,7 @@ def getCharacter(lookfor, eager=None): eager = processEager(eager) with sd_lock: character = saveddata_session.query(Character).options(*eager).filter( - Character.savedName == lookfor).first() + Character.savedName == lookfor).first() else: raise TypeError("Need integer or string as argument") return character @@ -241,22 +243,34 @@ def getFitsWithShip(shipID, ownerID=None, where=None, eager=None): return fits -def getBoosterFits(ownerID=None, where=None, eager=None): - """ - Get all the fits that are flagged as a boosting ship - If no user is passed, do this for all users. - """ - - if ownerID is not None and not isinstance(ownerID, int): - raise TypeError("OwnerID must be integer") - filter = Fit.booster == 1 - if ownerID is not None: - filter = and_(filter, Fit.ownerID == ownerID) - - filter = processWhere(filter, where) +def getRecentFits(ownerID=None, where=None, eager=None): eager = processEager(eager) with sd_lock: - fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all()) + q = select(( + Fit.ID, + Fit.shipID, + Fit.name, + Fit.modified, + Fit.created, + Fit.timestamp, + Fit.notes + )).order_by(desc(Fit.modified), desc(Fit.timestamp)).limit(50) + fits = eos.db.saveddata_session.execute(q).fetchall() + + return fits + + +def getFitsWithModules(typeIDs, eager=None): + """ + Get all the fits that have typeIDs fitted to them + """ + + if not hasattr(typeIDs, "__iter__"): + typeIDs = (typeIDs,) + + eager = processEager(eager) + with sd_lock: + fits = removeInvalid(saveddata_session.query(Fit).join(Module).options(*eager).filter(Module.itemID.in_(typeIDs)).all()) return fits @@ -336,6 +350,13 @@ def getDamagePatternList(eager=None): return patterns +def clearDamagePatterns(): + with sd_lock: + deleted_rows = saveddata_session.query(DamagePattern).filter(DamagePattern.name != 'Uniform').delete() + commit() + return deleted_rows + + def getTargetResistsList(eager=None): eager = processEager(eager) with sd_lock: @@ -343,6 +364,13 @@ def getTargetResistsList(eager=None): return patterns +def clearTargetResists(): + with sd_lock: + deleted_rows = saveddata_session.query(TargetResists).delete() + commit() + return deleted_rows + + def getImplantSetList(eager=None): eager = processEager(eager) with sd_lock: @@ -360,12 +388,12 @@ def getDamagePattern(lookfor, eager=None): eager = processEager(eager) with sd_lock: pattern = saveddata_session.query(DamagePattern).options(*eager).filter( - DamagePattern.ID == lookfor).first() + DamagePattern.ID == lookfor).first() elif isinstance(lookfor, basestring): eager = processEager(eager) with sd_lock: pattern = saveddata_session.query(DamagePattern).options(*eager).filter( - DamagePattern.name == lookfor).first() + DamagePattern.name == lookfor).first() else: raise TypeError("Need integer or string as argument") return pattern @@ -381,12 +409,12 @@ def getTargetResists(lookfor, eager=None): eager = processEager(eager) with sd_lock: pattern = saveddata_session.query(TargetResists).options(*eager).filter( - TargetResists.ID == lookfor).first() + TargetResists.ID == lookfor).first() elif isinstance(lookfor, basestring): eager = processEager(eager) with sd_lock: pattern = saveddata_session.query(TargetResists).options(*eager).filter( - TargetResists.name == lookfor).first() + TargetResists.name == lookfor).first() else: raise TypeError("Need integer or string as argument") return pattern @@ -402,7 +430,7 @@ def getImplantSet(lookfor, eager=None): eager = processEager(eager) with sd_lock: pattern = saveddata_session.query(ImplantSet).options(*eager).filter( - TargetResists.ID == lookfor).first() + TargetResists.ID == lookfor).first() elif isinstance(lookfor, basestring): eager = processEager(eager) with sd_lock: diff --git a/eos/db/saveddata/skill.py b/eos/db/saveddata/skill.py index 5b279e0d3..5e56f2cdb 100644 --- a/eos/db/saveddata/skill.py +++ b/eos/db/saveddata/skill.py @@ -17,15 +17,20 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, ForeignKey +from sqlalchemy import Table, Column, Integer, ForeignKey, DateTime from sqlalchemy.orm import mapper +import datetime from eos.db import saveddata_meta from eos.saveddata.character import Skill + skills_table = Table("characterSkills", saveddata_meta, Column("characterID", ForeignKey("characters.ID"), primary_key=True, index=True), Column("itemID", Integer, primary_key=True), - Column("_Skill__level", Integer, nullable=True)) + Column("_Skill__level", Integer, nullable=True), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) + ) mapper(Skill, skills_table) diff --git a/eos/db/saveddata/targetResists.py b/eos/db/saveddata/targetResists.py index e8f125c5c..f100bc2dd 100644 --- a/eos/db/saveddata/targetResists.py +++ b/eos/db/saveddata/targetResists.py @@ -17,8 +17,9 @@ # along with eos. If not, see . # =============================================================================== -from sqlalchemy import Table, Column, Integer, Float, ForeignKey, String +from sqlalchemy import Table, Column, Integer, Float, ForeignKey, String, DateTime from sqlalchemy.orm import mapper +import datetime from eos.db import saveddata_meta from eos.saveddata.targetResists import TargetResists @@ -30,6 +31,9 @@ targetResists_table = Table("targetResists", saveddata_meta, Column("thermalAmount", Float), Column("kineticAmount", Float), Column("explosiveAmount", Float), - Column("ownerID", ForeignKey("users.ID"), nullable=True)) + Column("ownerID", ForeignKey("users.ID"), nullable=True), + Column("created", DateTime, nullable=True, default=datetime.datetime.now), + Column("modified", DateTime, nullable=True, onupdate=datetime.datetime.now) + ) mapper(TargetResists, targetResists_table) diff --git a/eos/db/util.py b/eos/db/util.py index 151e2e38b..03a09aceb 100644 --- a/eos/db/util.py +++ b/eos/db/util.py @@ -20,16 +20,18 @@ from sqlalchemy.orm import eagerload from sqlalchemy.sql import and_ -replace = {"attributes": "_Item__attributes", - "modules": "_Fit__modules", - "projectedModules": "_Fit__projectedModules", - "boosters": "_Fit__boosters", - "drones": "_Fit__drones", - "projectedDrones": "_Fit__projectedDrones", - "implants": "_Fit__implants", - "character": "_Fit__character", - "damagePattern": "_Fit__damagePattern", - "projectedFits": "_Fit__projectedFits"} +replace = { + "attributes" : "_Item__attributes", + "modules" : "_Fit__modules", + "projectedModules": "_Fit__projectedModules", + "boosters" : "_Fit__boosters", + "drones" : "_Fit__drones", + "projectedDrones" : "_Fit__projectedDrones", + "implants" : "_Fit__implants", + "character" : "_Fit__character", + "damagePattern" : "_Fit__damagePattern", + "projectedFits" : "_Fit__projectedFits" +} def processEager(eager): diff --git a/eos/effects/adaptivearmorhardener.py b/eos/effects/adaptivearmorhardener.py index 8d017715d..246c67fcf 100644 --- a/eos/effects/adaptivearmorhardener.py +++ b/eos/effects/adaptivearmorhardener.py @@ -3,6 +3,7 @@ # Used by: # Module: Reactive Armor Hardener from logbook import Logger +import eos.config pyfalog = Logger(__name__) @@ -13,6 +14,12 @@ type = "active" def handler(fit, module, context): damagePattern = fit.damagePattern + static_adaptive_behavior = eos.config.settings['useStaticAdaptiveArmorHardener'] + + if (damagePattern.emAmount == damagePattern.thermalAmount == damagePattern.kineticAmount == damagePattern.explosiveAmount) and static_adaptive_behavior: + pyfalog.debug("Setting adaptivearmorhardener resists to uniform profile.") + return + # Skip if there is no damage pattern. Example: projected ships or fleet boosters if damagePattern: diff --git a/eos/effects/ammoinfluencecapneed.py b/eos/effects/ammoinfluencecapneed.py index c30a91bd1..ee8a2229d 100644 --- a/eos/effects/ammoinfluencecapneed.py +++ b/eos/effects/ammoinfluencecapneed.py @@ -1,7 +1,7 @@ # ammoInfluenceCapNeed # # Used by: -# Items from category: Charge (465 of 912) +# Items from category: Charge (466 of 913) # Charges from group: Frequency Crystal (185 of 185) # Charges from group: Hybrid Charge (209 of 209) type = "passive" diff --git a/eos/effects/ammoinfluencerange.py b/eos/effects/ammoinfluencerange.py index ea6860bbe..2a42d411c 100644 --- a/eos/effects/ammoinfluencerange.py +++ b/eos/effects/ammoinfluencerange.py @@ -1,7 +1,7 @@ # ammoInfluenceRange # # Used by: -# Items from category: Charge (571 of 912) +# Items from category: Charge (571 of 913) type = "passive" diff --git a/eos/effects/commandburstaoerolebonus.py b/eos/effects/commandburstaoerolebonus.py index 63de8ca87..99576c074 100644 --- a/eos/effects/commandburstaoerolebonus.py +++ b/eos/effects/commandburstaoerolebonus.py @@ -4,9 +4,9 @@ # Ships from group: Carrier (4 of 4) # Ships from group: Combat Battlecruiser (13 of 13) # Ships from group: Command Ship (8 of 8) -# Ships from group: Force Auxiliary (4 of 4) +# Ships from group: Force Auxiliary (5 of 5) # Ships from group: Supercarrier (6 of 6) -# Ships from group: Titan (5 of 5) +# Ships from group: Titan (6 of 6) # Subsystems named like: Defensive Warfare Processor (4 of 4) # Ship: Orca # Ship: Rorqual diff --git a/eos/effects/concordsecstatustankbonus.py b/eos/effects/concordsecstatustankbonus.py new file mode 100644 index 000000000..0dd54a483 --- /dev/null +++ b/eos/effects/concordsecstatustankbonus.py @@ -0,0 +1,22 @@ +# concordSecStatusTankBonus +# +# Used by: +# Ship: Enforcer +# Ship: Pacifier +type = "passive" + + +def handler(fit, src, context): + + # Get pilot sec status bonus directly here, instead of going through the intermediary effects + # via https://forums.eveonline.com/default.aspx?g=posts&t=515826 + try: + bonus = max(0, min(50.0, (src.parent.character.secStatus * 10))) + except: + bonus = None + + if bonus is not None: + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Repair Systems"), + "armorDamageAmount", bonus, stackingPenalties=True) + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Shield Operation"), + "shieldBonus", bonus, stackingPenalties=True) diff --git a/eos/effects/covertopsandreconopscloakmoduledelaybonus.py b/eos/effects/covertopsandreconopscloakmoduledelaybonus.py index 0a3e341c3..3718c9101 100644 --- a/eos/effects/covertopsandreconopscloakmoduledelaybonus.py +++ b/eos/effects/covertopsandreconopscloakmoduledelaybonus.py @@ -3,9 +3,9 @@ # Used by: # Ships from group: Black Ops (4 of 4) # Ships from group: Blockade Runner (4 of 4) -# Ships from group: Covert Ops (6 of 6) +# Ships from group: Covert Ops (7 of 7) # Ships from group: Expedition Frigate (2 of 2) -# Ships from group: Force Recon Ship (6 of 6) +# Ships from group: Force Recon Ship (7 of 7) # Ships from group: Stealth Bomber (4 of 4) # Ships named like: Stratios (2 of 2) # Subsystems named like: Offensive Covert Reconfiguration (4 of 4) diff --git a/eos/effects/covertopscloakcpupercentbonus1.py b/eos/effects/covertopscloakcpupercentbonus1.py index 7ac0f5523..e7763ec56 100644 --- a/eos/effects/covertopscloakcpupercentbonus1.py +++ b/eos/effects/covertopscloakcpupercentbonus1.py @@ -1,7 +1,7 @@ # covertOpsCloakCpuPercentBonus1 # # Used by: -# Ships from group: Covert Ops (5 of 6) +# Ships from group: Covert Ops (5 of 7) type = "passive" runTime = "early" diff --git a/eos/effects/covertopscloakcpupercentbonuspiratefaction.py b/eos/effects/covertopscloakcpupercentrolebonus.py similarity index 78% rename from eos/effects/covertopscloakcpupercentbonuspiratefaction.py rename to eos/effects/covertopscloakcpupercentrolebonus.py index 6feae8252..fea79b8e2 100644 --- a/eos/effects/covertopscloakcpupercentbonuspiratefaction.py +++ b/eos/effects/covertopscloakcpupercentrolebonus.py @@ -1,8 +1,10 @@ -# covertOpsCloakCPUPercentBonusPirateFaction +# covertOpsCloakCPUPercentRoleBonus # # Used by: # Ships from group: Expedition Frigate (2 of 2) # Ship: Astero +# Ship: Enforcer +# Ship: Pacifier # Ship: Victorieux Luxury Yacht type = "passive" runTime = "early" @@ -10,4 +12,4 @@ runTime = "early" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Cloaking"), - "cpu", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "cpu", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/cynosuraldurationbonus.py b/eos/effects/cynosuraldurationbonus.py index 743d13d9e..388e64125 100644 --- a/eos/effects/cynosuraldurationbonus.py +++ b/eos/effects/cynosuraldurationbonus.py @@ -1,7 +1,7 @@ # cynosuralDurationBonus # # Used by: -# Ships from group: Force Recon Ship (5 of 6) +# Ships from group: Force Recon Ship (6 of 7) type = "passive" diff --git a/eos/effects/cynosuraltheoryconsumptionbonus.py b/eos/effects/cynosuraltheoryconsumptionbonus.py index 0f66b0152..3d1be5194 100644 --- a/eos/effects/cynosuraltheoryconsumptionbonus.py +++ b/eos/effects/cynosuraltheoryconsumptionbonus.py @@ -1,7 +1,7 @@ # cynosuralTheoryConsumptionBonus # # Used by: -# Ships from group: Force Recon Ship (5 of 6) +# Ships from group: Force Recon Ship (6 of 7) # Skill: Cynosural Field Theory type = "passive" diff --git a/eos/effects/ecmburstjammer.py b/eos/effects/ecmburstjammer.py index 1d733e650..f21b89391 100644 --- a/eos/effects/ecmburstjammer.py +++ b/eos/effects/ecmburstjammer.py @@ -2,12 +2,17 @@ # # Used by: # Modules from group: Burst Jammer (11 of 11) +from eos.modifiedAttributeDict import ModifiedAttributeDict + type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, **kwargs): if "projected" in context: # jam formula: 1 - (1- (jammer str/ship str))^(# of jam mods with same str)) strModifier = 1 - module.getModifiedItemAttr("scan{0}StrengthBonus".format(fit.scanType)) / fit.scanStrength + if 'effect' in kwargs: + strModifier *= ModifiedAttributeDict.getResistance(fit, kwargs['effect']) + fit.ecmProjectedStr *= strModifier diff --git a/eos/effects/elitebonuscoveropsscanprobestrength2.py b/eos/effects/elitebonuscoveropsscanprobestrength2.py index 5d6dfc8c6..7b3df9a4b 100644 --- a/eos/effects/elitebonuscoveropsscanprobestrength2.py +++ b/eos/effects/elitebonuscoveropsscanprobestrength2.py @@ -1,7 +1,7 @@ # eliteBonusCoverOpsScanProbeStrength2 # # Used by: -# Ships from group: Covert Ops (6 of 6) +# Ships from group: Covert Ops (7 of 7) type = "passive" diff --git a/eos/effects/elitebonuscoveropswarpvelocity1.py b/eos/effects/elitebonuscoveropswarpvelocity1.py new file mode 100644 index 000000000..77c3b560e --- /dev/null +++ b/eos/effects/elitebonuscoveropswarpvelocity1.py @@ -0,0 +1,9 @@ +# eliteBonusCoverOpsWarpVelocity1 +# +# Used by: +# Ship: Pacifier +type = "passive" + + +def handler(fit, src, context): + fit.ship.boostItemAttr("warpSpeedMultiplier", src.getModifiedItemAttr("eliteBonusCoverOps1"), skill="Covert Ops") diff --git a/eos/effects/elitebonusreconwarpvelocity3.py b/eos/effects/elitebonusreconwarpvelocity3.py new file mode 100644 index 000000000..909c577a1 --- /dev/null +++ b/eos/effects/elitebonusreconwarpvelocity3.py @@ -0,0 +1,9 @@ +# eliteBonusReconWarpVelocity3 +# +# Used by: +# Ship: Enforcer +type = "passive" + + +def handler(fit, src, context): + fit.ship.boostItemAttr("warpSpeedMultiplier", src.getModifiedItemAttr("eliteBonusReconShip3"), skill="Recon Ships") diff --git a/eos/effects/elitereconjumpscramblerrangebonus2.py b/eos/effects/elitereconscramblerrangebonus2.py similarity index 85% rename from eos/effects/elitereconjumpscramblerrangebonus2.py rename to eos/effects/elitereconscramblerrangebonus2.py index b9bebe9a9..d308a56ba 100644 --- a/eos/effects/elitereconjumpscramblerrangebonus2.py +++ b/eos/effects/elitereconscramblerrangebonus2.py @@ -1,7 +1,8 @@ -# eliteReconJumpScramblerRangeBonus2 +# eliteReconScramblerRangeBonus2 # # Used by: # Ship: Arazu +# Ship: Enforcer # Ship: Lachesis type = "passive" diff --git a/eos/effects/elitereconstasiswebbonus1.py b/eos/effects/elitereconstasiswebbonus1.py new file mode 100644 index 000000000..abbf4f6c6 --- /dev/null +++ b/eos/effects/elitereconstasiswebbonus1.py @@ -0,0 +1,9 @@ +# eliteReconStasisWebBonus1 +# +# Used by: +# Ship: Enforcer +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Stasis Web", "maxRange", src.getModifiedItemAttr("eliteBonusReconShip1"), skill="Recon Ships") diff --git a/eos/effects/energyneutralizerfalloff.py b/eos/effects/energyneutralizerfalloff.py index bdaaa2d3a..37d829baa 100644 --- a/eos/effects/energyneutralizerfalloff.py +++ b/eos/effects/energyneutralizerfalloff.py @@ -3,14 +3,19 @@ # Used by: # Modules from group: Energy Neutralizer (51 of 51) from eos.saveddata.module import State +from eos.modifiedAttributeDict import ModifiedAttributeDict type = "active", "projected" -def handler(fit, src, context): +def handler(fit, src, context, **kwargs): if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or hasattr(src, "amountActive")): amount = src.getModifiedItemAttr("energyNeutralizerAmount") + + if 'effect' in kwargs: + amount *= ModifiedAttributeDict.getResistance(fit, kwargs['effect']) + time = src.getModifiedItemAttr("duration") fit.addDrain(src, time, amount, 0) diff --git a/eos/effects/energynosferatufalloff.py b/eos/effects/energynosferatufalloff.py index 15c7d20ff..841efb7cf 100644 --- a/eos/effects/energynosferatufalloff.py +++ b/eos/effects/energynosferatufalloff.py @@ -2,14 +2,19 @@ # # Used by: # Modules from group: Energy Nosferatu (51 of 51) +from eos.modifiedAttributeDict import ModifiedAttributeDict + type = "active", "projected" runTime = "late" -def handler(fit, src, context): +def handler(fit, src, context, **kwargs): amount = src.getModifiedItemAttr("powerTransferAmount") time = src.getModifiedItemAttr("duration") + if 'effect' in kwargs: + amount *= ModifiedAttributeDict.getResistance(fit, kwargs['effect']) + if "projected" in context: fit.addDrain(src, time, amount, 0) elif "module" in context: diff --git a/eos/effects/entityecmfalloff.py b/eos/effects/entityecmfalloff.py index 5d3ec62e9..9e382a641 100644 --- a/eos/effects/entityecmfalloff.py +++ b/eos/effects/entityecmfalloff.py @@ -2,12 +2,17 @@ # # Used by: # Drones named like: EC (3 of 3) +from eos.modifiedAttributeDict import ModifiedAttributeDict + type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, **kwargs): if "projected" in context: # jam formula: 1 - (1- (jammer str/ship str))^(# of jam mods with same str)) strModifier = 1 - module.getModifiedItemAttr("scan{0}StrengthBonus".format(fit.scanType)) / fit.scanStrength + if 'effect' in kwargs: + strModifier *= ModifiedAttributeDict.getResistance(fit, kwargs['effect']) + fit.ecmProjectedStr *= strModifier diff --git a/eos/effects/entityenergyneutralizerfalloff.py b/eos/effects/entityenergyneutralizerfalloff.py index f5aa5ead1..751cd540c 100644 --- a/eos/effects/entityenergyneutralizerfalloff.py +++ b/eos/effects/entityenergyneutralizerfalloff.py @@ -3,14 +3,18 @@ # Used by: # Drones from group: Energy Neutralizer Drone (3 of 3) from eos.saveddata.module import State +from eos.modifiedAttributeDict import ModifiedAttributeDict type = "active", "projected" -def handler(fit, src, context): +def handler(fit, src, context, **kwargs): if "projected" in context and ((hasattr(src, "state") and src.state >= State.ACTIVE) or hasattr(src, "amountActive")): amount = src.getModifiedItemAttr("energyNeutralizerAmount") time = src.getModifiedItemAttr("energyNeutralizerDuration") + if 'effect' in kwargs: + amount *= ModifiedAttributeDict.getResistance(fit, kwargs['effect']) + fit.addDrain(src, time, amount, 0) diff --git a/eos/effects/entosisdurationmultiply.py b/eos/effects/entosisdurationmultiply.py index 410ff6312..10d519002 100644 --- a/eos/effects/entosisdurationmultiply.py +++ b/eos/effects/entosisdurationmultiply.py @@ -1,12 +1,7 @@ # entosisDurationMultiply # # Used by: -# Ships from group: Carrier (4 of 4) -# Ships from group: Dreadnought (5 of 5) -# Ships from group: Force Auxiliary (4 of 4) -# Ships from group: Supercarrier (6 of 6) -# Ships from group: Titan (5 of 5) -# Ship: Rorqual +# Items from market group: Ships > Capital Ships (28 of 37) type = "passive" diff --git a/eos/effects/fighterabilityecm.py b/eos/effects/fighterabilityecm.py index 743df38ed..be4e41929 100644 --- a/eos/effects/fighterabilityecm.py +++ b/eos/effects/fighterabilityecm.py @@ -3,6 +3,7 @@ Since fighter abilities do not have any sort of item entity in the EVE database, we must derive the abilities from the effects, and thus this effect file contains some custom information useful only to fighters. """ +from eos.modifiedAttributeDict import ModifiedAttributeDict # User-friendly name for the ability displayName = "ECM" @@ -12,10 +13,13 @@ prefix = "fighterAbilityECM" type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, **kwargs): if "projected" not in context: return # jam formula: 1 - (1- (jammer str/ship str))^(# of jam mods with same str)) strModifier = 1 - module.getModifiedItemAttr("{}Strength{}".format(prefix, fit.scanType)) / fit.scanStrength + if 'effect' in kwargs: + strModifier *= ModifiedAttributeDict.getResistance(fit, kwargs['effect']) + fit.ecmProjectedStr *= strModifier diff --git a/eos/effects/fighterabilityenergyneutralizer.py b/eos/effects/fighterabilityenergyneutralizer.py index dd771b750..1e2734202 100644 --- a/eos/effects/fighterabilityenergyneutralizer.py +++ b/eos/effects/fighterabilityenergyneutralizer.py @@ -4,14 +4,19 @@ Since fighter abilities do not have any sort of item entity in the EVE database, effects, and thus this effect file contains some custom information useful only to fighters. """ # User-friendly name for the ability +from eos.modifiedAttributeDict import ModifiedAttributeDict + displayName = "Energy Neutralizer" prefix = "fighterAbilityEnergyNeutralizer" type = "active", "projected" -def handler(fit, src, context): +def handler(fit, src, context, **kwargs): if "projected" in context: amount = src.getModifiedItemAttr("{}Amount".format(prefix)) time = src.getModifiedItemAttr("{}Duration".format(prefix)) + if 'effect' in kwargs: + amount *= ModifiedAttributeDict.getResistance(fit, kwargs['effect']) + fit.addDrain(src, time, amount, 0) diff --git a/eos/effects/minigamevirusstrengthbonus.py b/eos/effects/minigamevirusstrengthbonus.py index 63c42ffdb..f346d387a 100644 --- a/eos/effects/minigamevirusstrengthbonus.py +++ b/eos/effects/minigamevirusstrengthbonus.py @@ -1,7 +1,7 @@ # minigameVirusStrengthBonus # # Used by: -# Ships from group: Covert Ops (6 of 6) +# Ships from group: Covert Ops (7 of 7) # Ships named like: Stratios (2 of 2) # Subsystems named like: Electronics Emergent Locus Analyzer (4 of 4) # Ship: Astero diff --git a/eos/effects/modulebonusancillaryremotearmorrepairer.py b/eos/effects/modulebonusancillaryremotearmorrepairer.py index 78a6ef855..95aa3c793 100644 --- a/eos/effects/modulebonusancillaryremotearmorrepairer.py +++ b/eos/effects/modulebonusancillaryremotearmorrepairer.py @@ -6,7 +6,7 @@ runTime = "late" type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, **kwargs): if "projected" not in context: return @@ -17,4 +17,4 @@ def handler(fit, module, context): amount = module.getModifiedItemAttr("armorDamageAmount") * multiplier speed = module.getModifiedItemAttr("duration") / 1000.0 - fit.extraAttributes.increase("armorRepair", amount / speed) + fit.extraAttributes.increase("armorRepair", amount / speed, **kwargs) diff --git a/eos/effects/modulebonusancillaryremoteshieldbooster.py b/eos/effects/modulebonusancillaryremoteshieldbooster.py index 99b1a444c..36271629e 100644 --- a/eos/effects/modulebonusancillaryremoteshieldbooster.py +++ b/eos/effects/modulebonusancillaryremoteshieldbooster.py @@ -6,9 +6,9 @@ runTime = "late" type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, **kwargs): if "projected" not in context: return amount = module.getModifiedItemAttr("shieldBonus") speed = module.getModifiedItemAttr("duration") / 1000.0 - fit.extraAttributes.increase("shieldRepair", amount / speed) + fit.extraAttributes.increase("shieldRepair", amount / speed, **kwargs) diff --git a/eos/effects/npcentityweapondisruptor.py b/eos/effects/npcentityweapondisruptor.py index e2fb7bf4a..28661af18 100644 --- a/eos/effects/npcentityweapondisruptor.py +++ b/eos/effects/npcentityweapondisruptor.py @@ -5,14 +5,14 @@ type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" in context: fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "trackingSpeed", module.getModifiedItemAttr("trackingSpeedBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "maxRange", module.getModifiedItemAttr("maxRangeBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "falloff", module.getModifiedItemAttr("falloffBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/reconshipcloakcpubonus1.py b/eos/effects/reconshipcloakcpubonus1.py index 602efab28..4032baabc 100644 --- a/eos/effects/reconshipcloakcpubonus1.py +++ b/eos/effects/reconshipcloakcpubonus1.py @@ -1,7 +1,7 @@ # reconShipCloakCpuBonus1 # # Used by: -# Ships from group: Force Recon Ship (6 of 6) +# Ships from group: Force Recon Ship (6 of 7) type = "passive" runTime = "early" diff --git a/eos/effects/remotearmorrepairfalloff.py b/eos/effects/remotearmorrepairfalloff.py index e20db1af6..9bce29829 100644 --- a/eos/effects/remotearmorrepairfalloff.py +++ b/eos/effects/remotearmorrepairfalloff.py @@ -5,8 +5,8 @@ type = "projected", "active" -def handler(fit, container, context): +def handler(fit, container, context, **kwargs): if "projected" in context: bonus = container.getModifiedItemAttr("armorDamageAmount") duration = container.getModifiedItemAttr("duration") / 1000.0 - fit.extraAttributes.increase("armorRepair", bonus / duration) + fit.extraAttributes.increase("armorRepair", bonus / duration, **kwargs) diff --git a/eos/effects/remoteecmfalloff.py b/eos/effects/remoteecmfalloff.py index 377887992..409235895 100644 --- a/eos/effects/remoteecmfalloff.py +++ b/eos/effects/remoteecmfalloff.py @@ -2,12 +2,17 @@ # # Used by: # Modules from group: ECM (39 of 39) +from eos.modifiedAttributeDict import ModifiedAttributeDict + type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, **kwargs): if "projected" in context: # jam formula: 1 - (1- (jammer str/ship str))^(# of jam mods with same str)) strModifier = 1 - module.getModifiedItemAttr("scan{0}StrengthBonus".format(fit.scanType)) / fit.scanStrength + if 'effect' in kwargs: + strModifier *= ModifiedAttributeDict.getResistance(fit, kwargs['effect']) + fit.ecmProjectedStr *= strModifier diff --git a/eos/effects/remotesensordampentity.py b/eos/effects/remotesensordampentity.py index adb0b1ac3..642b55411 100644 --- a/eos/effects/remotesensordampentity.py +++ b/eos/effects/remotesensordampentity.py @@ -5,12 +5,12 @@ type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" not in context: return fit.ship.boostItemAttr("maxTargetRange", module.getModifiedItemAttr("maxTargetRangeBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.ship.boostItemAttr("scanResolution", module.getModifiedItemAttr("scanResolutionBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/remotesensordampfalloff.py b/eos/effects/remotesensordampfalloff.py index e04b573c1..81ae5dfa1 100644 --- a/eos/effects/remotesensordampfalloff.py +++ b/eos/effects/remotesensordampfalloff.py @@ -5,12 +5,12 @@ type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" not in context: return fit.ship.boostItemAttr("maxTargetRange", module.getModifiedItemAttr("maxTargetRangeBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.ship.boostItemAttr("scanResolution", module.getModifiedItemAttr("scanResolutionBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/remoteshieldtransferfalloff.py b/eos/effects/remoteshieldtransferfalloff.py index 4bdeee8eb..f8fc36e60 100644 --- a/eos/effects/remoteshieldtransferfalloff.py +++ b/eos/effects/remoteshieldtransferfalloff.py @@ -5,8 +5,8 @@ type = "projected", "active" -def handler(fit, container, context): +def handler(fit, container, context, **kwargs): if "projected" in context: bonus = container.getModifiedItemAttr("shieldBonus") duration = container.getModifiedItemAttr("duration") / 1000.0 - fit.extraAttributes.increase("shieldRepair", bonus / duration) + fit.extraAttributes.increase("shieldRepair", bonus / duration, **kwargs) diff --git a/eos/effects/remotetargetpaintentity.py b/eos/effects/remotetargetpaintentity.py index 4ee7c9fc0..07b8c9169 100644 --- a/eos/effects/remotetargetpaintentity.py +++ b/eos/effects/remotetargetpaintentity.py @@ -5,7 +5,7 @@ type = "projected", "active" -def handler(fit, container, context): +def handler(fit, container, context, *args, **kwargs): if "projected" in context: fit.ship.boostItemAttr("signatureRadius", container.getModifiedItemAttr("signatureRadiusBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/remotetargetpaintfalloff.py b/eos/effects/remotetargetpaintfalloff.py index b9f5a6ef1..0e80806ca 100644 --- a/eos/effects/remotetargetpaintfalloff.py +++ b/eos/effects/remotetargetpaintfalloff.py @@ -5,7 +5,7 @@ type = "projected", "active" -def handler(fit, container, context): +def handler(fit, container, context, *args, **kwargs): if "projected" in context: fit.ship.boostItemAttr("signatureRadius", container.getModifiedItemAttr("signatureRadiusBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/remotewebifierentity.py b/eos/effects/remotewebifierentity.py index cf9905405..ca4b34de7 100644 --- a/eos/effects/remotewebifierentity.py +++ b/eos/effects/remotewebifierentity.py @@ -5,8 +5,8 @@ type = "active", "projected" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" not in context: return fit.ship.boostItemAttr("maxVelocity", module.getModifiedItemAttr("speedFactor"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/remotewebifierfalloff.py b/eos/effects/remotewebifierfalloff.py index 1db5eb858..4d5bc770c 100644 --- a/eos/effects/remotewebifierfalloff.py +++ b/eos/effects/remotewebifierfalloff.py @@ -6,8 +6,8 @@ type = "active", "projected" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" not in context: return fit.ship.boostItemAttr("maxVelocity", module.getModifiedItemAttr("speedFactor"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/scriptdurationbonus.py b/eos/effects/scriptdurationbonus.py index 5b3081529..23361f87b 100644 --- a/eos/effects/scriptdurationbonus.py +++ b/eos/effects/scriptdurationbonus.py @@ -1,7 +1,7 @@ # scriptDurationBonus # # Used by: -# Charge: Focused Warp Disruption Script +# Charges from group: Warp Disruption Script (2 of 2) type = "passive" diff --git a/eos/effects/scriptmassbonuspercentagebonus.py b/eos/effects/scriptmassbonuspercentagebonus.py index e6d42eee4..dcf4a6c02 100644 --- a/eos/effects/scriptmassbonuspercentagebonus.py +++ b/eos/effects/scriptmassbonuspercentagebonus.py @@ -1,7 +1,7 @@ # scriptMassBonusPercentageBonus # # Used by: -# Charge: Focused Warp Disruption Script +# Charges from group: Warp Disruption Script (2 of 2) type = "passive" runTime = "early" diff --git a/eos/effects/scriptsignatureradiusbonusbonus.py b/eos/effects/scriptsignatureradiusbonusbonus.py index 29b60408a..34ee9918f 100644 --- a/eos/effects/scriptsignatureradiusbonusbonus.py +++ b/eos/effects/scriptsignatureradiusbonusbonus.py @@ -1,7 +1,7 @@ # scriptSignatureRadiusBonusBonus # # Used by: -# Charge: Focused Warp Disruption Script +# Charges from group: Warp Disruption Script (2 of 2) type = "passive" runTime = "early" diff --git a/eos/effects/scriptspeedboostfactorbonusbonus.py b/eos/effects/scriptspeedboostfactorbonusbonus.py index e7795bc1a..7ead7eb03 100644 --- a/eos/effects/scriptspeedboostfactorbonusbonus.py +++ b/eos/effects/scriptspeedboostfactorbonusbonus.py @@ -1,7 +1,7 @@ # scriptSpeedBoostFactorBonusBonus # # Used by: -# Charge: Focused Warp Disruption Script +# Charges from group: Warp Disruption Script (2 of 2) type = "passive" runTime = "early" diff --git a/eos/effects/scriptspeedfactorbonusbonus.py b/eos/effects/scriptspeedfactorbonusbonus.py index 3df351500..f3d560d76 100644 --- a/eos/effects/scriptspeedfactorbonusbonus.py +++ b/eos/effects/scriptspeedfactorbonusbonus.py @@ -1,7 +1,7 @@ # scriptSpeedFactorBonusBonus # # Used by: -# Charge: Focused Warp Disruption Script +# Charges from group: Warp Disruption Script (2 of 2) type = "passive" runTime = "early" diff --git a/eos/effects/scriptwarpdisruptionfieldgeneratorsetdisallowinempirespace.py b/eos/effects/scriptwarpdisruptionfieldgeneratorsetdisallowinempirespace.py index 4e9f29bd9..4c224a0ef 100644 --- a/eos/effects/scriptwarpdisruptionfieldgeneratorsetdisallowinempirespace.py +++ b/eos/effects/scriptwarpdisruptionfieldgeneratorsetdisallowinempirespace.py @@ -1,7 +1,7 @@ # scriptWarpDisruptionFieldGeneratorSetDisallowInEmpireSpace # # Used by: -# Charge: Focused Warp Disruption Script +# Charges from group: Warp Disruption Script (2 of 2) type = "passive" diff --git a/eos/effects/scriptwarpscramblerangebonus.py b/eos/effects/scriptwarpscramblerangebonus.py index adbcbc88e..cc253590b 100644 --- a/eos/effects/scriptwarpscramblerangebonus.py +++ b/eos/effects/scriptwarpscramblerangebonus.py @@ -1,7 +1,7 @@ # scriptWarpScrambleRangeBonus # # Used by: -# Charge: Focused Warp Disruption Script +# Charges from group: Warp Disruption Script (2 of 2) type = "passive" diff --git a/eos/effects/shieldmanagementshieldcapacitybonuspostpercentcapacitylocationshipgroupshield.py b/eos/effects/shieldmanagementshieldcapacitybonuspostpercentcapacitylocationshipgroupshield.py index 9e2f091fe..d9209c608 100644 --- a/eos/effects/shieldmanagementshieldcapacitybonuspostpercentcapacitylocationshipgroupshield.py +++ b/eos/effects/shieldmanagementshieldcapacitybonuspostpercentcapacitylocationshipgroupshield.py @@ -3,7 +3,6 @@ # Used by: # Implants named like: Zainou 'Gnome' Shield Management SM (6 of 6) # Modules named like: Core Defense Field Extender (8 of 8) -# Modules named like: QA Multiship Module Players (4 of 4) # Implant: Genolution Core Augmentation CA-3 # Implant: Sansha Modified 'Gnome' Implant # Skill: Shield Management diff --git a/eos/effects/shipadvancedspaceshipcommandagilitybonus.py b/eos/effects/shipadvancedspaceshipcommandagilitybonus.py index 725dfb43d..d7e58addf 100644 --- a/eos/effects/shipadvancedspaceshipcommandagilitybonus.py +++ b/eos/effects/shipadvancedspaceshipcommandagilitybonus.py @@ -1,7 +1,7 @@ # shipAdvancedSpaceshipCommandAgilityBonus # # Used by: -# Items from market group: Ships > Capital Ships (34 of 34) +# Items from market group: Ships > Capital Ships (37 of 37) type = "passive" diff --git a/eos/effects/shipbonusdreadnoughta1energywarfareamountbonus.py b/eos/effects/shipbonusdreadnoughta1energywarfareamountbonus.py new file mode 100644 index 000000000..86e7725ac --- /dev/null +++ b/eos/effects/shipbonusdreadnoughta1energywarfareamountbonus.py @@ -0,0 +1,12 @@ +# shipBonusDreadnoughtA1EnergyWarfareAmountBonus +# +# Used by: +# Ship: Chemosh +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Energy Nosferatu", + "powerTransferAmount", src.getModifiedItemAttr("shipBonusDreadnoughtA1"), skill="Amarr Dreadnought") + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Energy Neutralizer", + "energyNeutralizerAmount", src.getModifiedItemAttr("shipBonusDreadnoughtA1"), skill="Amarr Dreadnought") diff --git a/eos/effects/shipbonusdreadnoughtm1webrangebonus.py b/eos/effects/shipbonusdreadnoughtm1webrangebonus.py new file mode 100644 index 000000000..f79762142 --- /dev/null +++ b/eos/effects/shipbonusdreadnoughtm1webrangebonus.py @@ -0,0 +1,10 @@ +# shipBonusDreadnoughtM1WebRangeBonus +# +# Used by: +# Ship: Chemosh +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Stasis Web", + "maxRange", src.getModifiedItemAttr("shipBonusDreadnoughtM1"), skill="Minmatar Dreadnought") diff --git a/eos/effects/shipbonusdreadnoughtm1webbonus.py b/eos/effects/shipbonusdreadnoughtm1webstrengthbonus.py similarity index 87% rename from eos/effects/shipbonusdreadnoughtm1webbonus.py rename to eos/effects/shipbonusdreadnoughtm1webstrengthbonus.py index 040ce5086..c822336ba 100644 --- a/eos/effects/shipbonusdreadnoughtm1webbonus.py +++ b/eos/effects/shipbonusdreadnoughtm1webstrengthbonus.py @@ -1,4 +1,4 @@ -# shipBonusDreadnoughtM1WebBonus +# shipBonusDreadnoughtM1WebStrengthBonus # # Used by: # Ship: Vehement diff --git a/eos/effects/shipbonusdronemwdboostrole.py b/eos/effects/shipbonusdronemwdboostrole.py index 6b342b2b0..a9960c71a 100644 --- a/eos/effects/shipbonusdronemwdboostrole.py +++ b/eos/effects/shipbonusdronemwdboostrole.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Drones"), - "maxVelocity", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "maxVelocity", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusforceauxiliarya1nosferatudrainamount.py b/eos/effects/shipbonusforceauxiliarya1nosferatudrainamount.py new file mode 100644 index 000000000..113c0e899 --- /dev/null +++ b/eos/effects/shipbonusforceauxiliarya1nosferatudrainamount.py @@ -0,0 +1,10 @@ +# shipBonusForceAuxiliaryA1NosferatuDrainAmount +# +# Used by: +# Ship: Dagon +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Energy Nosferatu", + "powerTransferAmount", src.getModifiedItemAttr("shipBonusForceAuxiliaryA1"), skill="Amarr Carrier") diff --git a/eos/effects/shipbonusforceauxiliarya1nosferaturangebonus.py b/eos/effects/shipbonusforceauxiliarya1nosferaturangebonus.py new file mode 100644 index 000000000..f3a1d2a62 --- /dev/null +++ b/eos/effects/shipbonusforceauxiliarya1nosferaturangebonus.py @@ -0,0 +1,12 @@ +# shipBonusForceAuxiliaryA1NosferatuRangeBonus +# +# Used by: +# Ship: Dagon +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Energy Nosferatu", + "maxRange", src.getModifiedItemAttr("shipBonusForceAuxiliaryA1"), skill="Amarr Carrier") + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Energy Nosferatu", + "falloffEffectiveness", src.getModifiedItemAttr("shipBonusForceAuxiliaryA1"), skill="Amarr Carrier") diff --git a/eos/effects/shipbonusforceauxiliarym1remotearmorrepairduration.py b/eos/effects/shipbonusforceauxiliarym1remotearmorrepairduration.py new file mode 100644 index 000000000..fd4dca81a --- /dev/null +++ b/eos/effects/shipbonusforceauxiliarym1remotearmorrepairduration.py @@ -0,0 +1,10 @@ +# shipBonusForceAuxiliaryM1RemoteArmorRepairDuration +# +# Used by: +# Ship: Dagon +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"), + "duration", src.getModifiedItemAttr("shipBonusForceAuxiliaryM1"), skill="Minmatar Carrier") diff --git a/eos/effects/shipbonusforceauxiliarym1remotecycletime.py b/eos/effects/shipbonusforceauxiliarym1remoteduration.py similarity index 94% rename from eos/effects/shipbonusforceauxiliarym1remotecycletime.py rename to eos/effects/shipbonusforceauxiliarym1remoteduration.py index 9ab5ae04c..71d8d8e07 100644 --- a/eos/effects/shipbonusforceauxiliarym1remotecycletime.py +++ b/eos/effects/shipbonusforceauxiliarym1remoteduration.py @@ -1,4 +1,4 @@ -# shipBonusForceAuxiliaryM1RemoteCycleTime +# shipBonusForceAuxiliaryM1RemoteDuration # # Used by: # Ship: Lif diff --git a/eos/effects/shipbonusforceauxiliarym2localrepairamount.py b/eos/effects/shipbonusforceauxiliarym2localrepairamount.py new file mode 100644 index 000000000..1379af95b --- /dev/null +++ b/eos/effects/shipbonusforceauxiliarym2localrepairamount.py @@ -0,0 +1,8 @@ +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Repair Systems"), + "armorDamageAmount", src.getModifiedItemAttr("shipBonusForceAuxiliaryM2"), skill="Minmatar Carrier") + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capital Repair Systems"), + "armorDamageAmount", src.getModifiedItemAttr("shipBonusForceAuxiliaryM2"), skill="Minmatar Carrier") diff --git a/eos/effects/shipbonusheavydronearmorhppiratefaction.py b/eos/effects/shipbonusheavydronearmorhppiratefaction.py index c5dc193ab..369005a2d 100644 --- a/eos/effects/shipbonusheavydronearmorhppiratefaction.py +++ b/eos/effects/shipbonusheavydronearmorhppiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Heavy Drone Operation"), - "armorHP", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "armorHP", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusheavydronedamagemultiplierpiratefaction.py b/eos/effects/shipbonusheavydronedamagemultiplierpiratefaction.py index b1a03b0f4..6682948a8 100644 --- a/eos/effects/shipbonusheavydronedamagemultiplierpiratefaction.py +++ b/eos/effects/shipbonusheavydronedamagemultiplierpiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Heavy Drone Operation"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusheavydronehppiratefaction.py b/eos/effects/shipbonusheavydronehppiratefaction.py index 58a3c7eb4..9c12a7101 100644 --- a/eos/effects/shipbonusheavydronehppiratefaction.py +++ b/eos/effects/shipbonusheavydronehppiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Heavy Drone Operation"), - "hp", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "hp", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusheavydroneshieldhppiratefaction.py b/eos/effects/shipbonusheavydroneshieldhppiratefaction.py index 60d10086f..f2575bf91 100644 --- a/eos/effects/shipbonusheavydroneshieldhppiratefaction.py +++ b/eos/effects/shipbonusheavydroneshieldhppiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Heavy Drone Operation"), - "shieldCapacity", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "shieldCapacity", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonushybridtrackinggf2.py b/eos/effects/shipbonushybridtrackinggf2.py index dabf163d6..77b58a6d6 100644 --- a/eos/effects/shipbonushybridtrackinggf2.py +++ b/eos/effects/shipbonushybridtrackinggf2.py @@ -3,6 +3,7 @@ # Used by: # Ship: Ares # Ship: Federation Navy Comet +# Ship: Pacifier # Ship: Tristan type = "passive" diff --git a/eos/effects/shipbonusletoptimalrangepiratefaction.py b/eos/effects/shipbonusletoptimalrangepiratefaction.py index d2f044b5f..680c7ef2d 100644 --- a/eos/effects/shipbonusletoptimalrangepiratefaction.py +++ b/eos/effects/shipbonusletoptimalrangepiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Large Energy Turret"), - "maxRange", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "maxRange", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonuslightdronearmorhppiratefaction.py b/eos/effects/shipbonuslightdronearmorhppiratefaction.py index aaefc8119..7f4706023 100644 --- a/eos/effects/shipbonuslightdronearmorhppiratefaction.py +++ b/eos/effects/shipbonuslightdronearmorhppiratefaction.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Light Drone Operation"), - "armorHP", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "armorHP", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonuslightdronedamagemultiplierpiratefaction.py b/eos/effects/shipbonuslightdronedamagemultiplierpiratefaction.py index e5a9b340c..75451999c 100644 --- a/eos/effects/shipbonuslightdronedamagemultiplierpiratefaction.py +++ b/eos/effects/shipbonuslightdronedamagemultiplierpiratefaction.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Light Drone Operation"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonuslightdronehppiratefaction.py b/eos/effects/shipbonuslightdronehppiratefaction.py index af6b5dae4..2511b9736 100644 --- a/eos/effects/shipbonuslightdronehppiratefaction.py +++ b/eos/effects/shipbonuslightdronehppiratefaction.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Light Drone Operation"), - "hp", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "hp", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonuslightdroneshieldhppiratefaction.py b/eos/effects/shipbonuslightdroneshieldhppiratefaction.py index 8bbff53f2..0e7b7b147 100644 --- a/eos/effects/shipbonuslightdroneshieldhppiratefaction.py +++ b/eos/effects/shipbonuslightdroneshieldhppiratefaction.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Light Drone Operation"), - "shieldCapacity", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "shieldCapacity", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusmediumdronearmorhppiratefaction.py b/eos/effects/shipbonusmediumdronearmorhppiratefaction.py index e48c99319..2d5373b43 100644 --- a/eos/effects/shipbonusmediumdronearmorhppiratefaction.py +++ b/eos/effects/shipbonusmediumdronearmorhppiratefaction.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Medium Drone Operation"), - "armorHP", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "armorHP", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusmediumdronedamagemultiplierpiratefaction.py b/eos/effects/shipbonusmediumdronedamagemultiplierpiratefaction.py index 6adbcb51b..627698037 100644 --- a/eos/effects/shipbonusmediumdronedamagemultiplierpiratefaction.py +++ b/eos/effects/shipbonusmediumdronedamagemultiplierpiratefaction.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Medium Drone Operation"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusmediumdronehppiratefaction.py b/eos/effects/shipbonusmediumdronehppiratefaction.py index 287bd32ad..3a41ee95c 100644 --- a/eos/effects/shipbonusmediumdronehppiratefaction.py +++ b/eos/effects/shipbonusmediumdronehppiratefaction.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Medium Drone Operation"), - "hp", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "hp", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusmediumdroneshieldhppiratefaction.py b/eos/effects/shipbonusmediumdroneshieldhppiratefaction.py index eb68ee3fb..593009a33 100644 --- a/eos/effects/shipbonusmediumdroneshieldhppiratefaction.py +++ b/eos/effects/shipbonusmediumdroneshieldhppiratefaction.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Medium Drone Operation"), - "shieldCapacity", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "shieldCapacity", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusmediumenergyturretdamagepiratefaction.py b/eos/effects/shipbonusmediumenergyturretdamagepiratefaction.py index 428e548a0..8e3662ced 100644 --- a/eos/effects/shipbonusmediumenergyturretdamagepiratefaction.py +++ b/eos/effects/shipbonusmediumenergyturretdamagepiratefaction.py @@ -10,4 +10,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Medium Energy Turret"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusmedmissileflighttimecc2.py b/eos/effects/shipbonusmedmissileflighttimecc2.py new file mode 100644 index 000000000..a8d76a49e --- /dev/null +++ b/eos/effects/shipbonusmedmissileflighttimecc2.py @@ -0,0 +1,12 @@ +# shipBonusMedMissileFlightTimeCC2 +# +# Used by: +# Ship: Enforcer +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Missiles"), + "explosionDelay", src.getModifiedItemAttr("shipBonusCC2"), skill="Caldari Cruiser") + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Assault Missiles"), + "explosionDelay", src.getModifiedItemAttr("shipBonusCC2"), skill="Caldari Cruiser") diff --git a/eos/effects/shipbonusmetoptimalac2.py b/eos/effects/shipbonusmetoptimalac2.py index 50d9ce4a4..758ede0d7 100644 --- a/eos/effects/shipbonusmetoptimalac2.py +++ b/eos/effects/shipbonusmetoptimalac2.py @@ -1,6 +1,7 @@ # shipBonusMETOptimalAC2 # # Used by: +# Ship: Enforcer # Ship: Omen Navy Issue type = "passive" diff --git a/eos/effects/shipbonusmetoptimalrangepiratefaction.py b/eos/effects/shipbonusmetoptimalrangepiratefaction.py index 5380edc07..10451c94f 100644 --- a/eos/effects/shipbonusmetoptimalrangepiratefaction.py +++ b/eos/effects/shipbonusmetoptimalrangepiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Medium Energy Turret"), - "maxRange", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "maxRange", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonuspiratefrigateprojdamage.py b/eos/effects/shipbonuspiratefrigateprojdamage.py index 406b847e9..e3710e611 100644 --- a/eos/effects/shipbonuspiratefrigateprojdamage.py +++ b/eos/effects/shipbonuspiratefrigateprojdamage.py @@ -10,4 +10,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Projectile Turret"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonuspiratesmallhybriddmg.py b/eos/effects/shipbonuspiratesmallhybriddmg.py index 596934d24..a9a7fb9eb 100644 --- a/eos/effects/shipbonuspiratesmallhybriddmg.py +++ b/eos/effects/shipbonuspiratesmallhybriddmg.py @@ -9,4 +9,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Hybrid Turret"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusremoterepairamountpiratefaction.py b/eos/effects/shipbonusremoterepairamountpiratefaction.py index f8152a605..3ac0e69fb 100644 --- a/eos/effects/shipbonusremoterepairamountpiratefaction.py +++ b/eos/effects/shipbonusremoterepairamountpiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"), - "armorDamageAmount", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "armorDamageAmount", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonusdreadnoughtrole1damagebonus.py b/eos/effects/shipbonusrole1capitalhybriddamagebonus.py similarity index 87% rename from eos/effects/shipbonusdreadnoughtrole1damagebonus.py rename to eos/effects/shipbonusrole1capitalhybriddamagebonus.py index 0cba1617a..4144b30b4 100644 --- a/eos/effects/shipbonusdreadnoughtrole1damagebonus.py +++ b/eos/effects/shipbonusrole1capitalhybriddamagebonus.py @@ -1,4 +1,4 @@ -# shipBonusDreadnoughtRole1DamageBonus +# shipBonusRole1CapitalHybridDamageBonus # # Used by: # Ship: Vehement diff --git a/eos/effects/shipbonusforceauxiliaryrole1cpubonus.py b/eos/effects/shipbonusrole1commandburstcpubonus.py similarity index 73% rename from eos/effects/shipbonusforceauxiliaryrole1cpubonus.py rename to eos/effects/shipbonusrole1commandburstcpubonus.py index cda897b72..1959f3821 100644 --- a/eos/effects/shipbonusforceauxiliaryrole1cpubonus.py +++ b/eos/effects/shipbonusrole1commandburstcpubonus.py @@ -1,7 +1,7 @@ -# shipBonusForceAuxiliaryRole1CPUBonus +# shipBonusRole1CommandBurstCPUBonus # # Used by: -# Ships from group: Force Auxiliary (4 of 4) +# Ships from group: Force Auxiliary (5 of 5) type = "passive" diff --git a/eos/effects/shipbonustitanrole1numwarfarelinks.py b/eos/effects/shipbonusrole1numwarfarelinks.py similarity index 77% rename from eos/effects/shipbonustitanrole1numwarfarelinks.py rename to eos/effects/shipbonusrole1numwarfarelinks.py index 67a017e32..23ca99188 100644 --- a/eos/effects/shipbonustitanrole1numwarfarelinks.py +++ b/eos/effects/shipbonusrole1numwarfarelinks.py @@ -1,7 +1,7 @@ -# shipBonusTitanRole1NumWarfareLinks +# shipBonusRole1NumWarfareLinks # # Used by: -# Ships from group: Titan (5 of 5) +# Ships from group: Titan (6 of 6) type = "passive" diff --git a/eos/effects/shipbonustitanrole2armorshieldmodulebonus.py b/eos/effects/shipbonusrole2armorplatesshieldextendersbonus.py similarity index 83% rename from eos/effects/shipbonustitanrole2armorshieldmodulebonus.py rename to eos/effects/shipbonusrole2armorplatesshieldextendersbonus.py index a964d0635..5711fd4e4 100644 --- a/eos/effects/shipbonustitanrole2armorshieldmodulebonus.py +++ b/eos/effects/shipbonusrole2armorplatesshieldextendersbonus.py @@ -1,7 +1,7 @@ -# shipBonusTitanRole2ArmorShieldModuleBonus +# shipBonusRole2ArmorPlates&ShieldExtendersBonus # # Used by: -# Ships from group: Titan (5 of 5) +# Ships from group: Titan (6 of 6) type = "passive" diff --git a/eos/effects/shipbonusforceauxiliaryrole2logisticdronebonus.py b/eos/effects/shipbonusrole2logisticdronebonus.py similarity index 87% rename from eos/effects/shipbonusforceauxiliaryrole2logisticdronebonus.py rename to eos/effects/shipbonusrole2logisticdronebonus.py index 53bb50f5f..7bd585081 100644 --- a/eos/effects/shipbonusforceauxiliaryrole2logisticdronebonus.py +++ b/eos/effects/shipbonusrole2logisticdronebonus.py @@ -1,7 +1,7 @@ -# shipBonusForceAuxiliaryRole2LogisticDroneBonus +# shipBonusRole2LogisticDroneBonus # # Used by: -# Ships from group: Force Auxiliary (4 of 4) +# Ships from group: Force Auxiliary (5 of 5) type = "passive" diff --git a/eos/effects/shipbonusrole3capitalenergydamagebonus.py b/eos/effects/shipbonusrole3capitalenergydamagebonus.py new file mode 100644 index 000000000..d21771a03 --- /dev/null +++ b/eos/effects/shipbonusrole3capitalenergydamagebonus.py @@ -0,0 +1,10 @@ +# shipBonusRole3CapitalEnergyDamageBonus +# +# Used by: +# Ship: Chemosh +# Ship: Molok +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capital Energy Turret"), "damageMultiplier", src.getModifiedItemAttr("shipBonusRole3")) diff --git a/eos/effects/shipbonustitanrole3damagebonus.py b/eos/effects/shipbonusrole3capitalhybriddamagebonus.py similarity index 87% rename from eos/effects/shipbonustitanrole3damagebonus.py rename to eos/effects/shipbonusrole3capitalhybriddamagebonus.py index 1bb3e16a6..c79148d3e 100644 --- a/eos/effects/shipbonustitanrole3damagebonus.py +++ b/eos/effects/shipbonusrole3capitalhybriddamagebonus.py @@ -1,4 +1,4 @@ -# shipBonusTitanRole3DamageBonus +# shipBonusRole3CapitalHybridDamageBonus # # Used by: # Ship: Vanquisher diff --git a/eos/effects/shipbonusforceauxiliaryrole3numwarfarelinks.py b/eos/effects/shipbonusrole3numwarfarelinks.py similarity index 73% rename from eos/effects/shipbonusforceauxiliaryrole3numwarfarelinks.py rename to eos/effects/shipbonusrole3numwarfarelinks.py index 69a19a64b..28f769880 100644 --- a/eos/effects/shipbonusforceauxiliaryrole3numwarfarelinks.py +++ b/eos/effects/shipbonusrole3numwarfarelinks.py @@ -1,7 +1,7 @@ -# shipBonusForceAuxiliaryRole3NumWarfareLinks +# shipBonusRole3NumWarfareLinks # # Used by: -# Ships from group: Force Auxiliary (4 of 4) +# Ships from group: Force Auxiliary (5 of 5) type = "passive" diff --git a/eos/effects/shipbonusrole4nosferatucpubonus.py b/eos/effects/shipbonusrole4nosferatucpubonus.py new file mode 100644 index 000000000..1d67fbb4b --- /dev/null +++ b/eos/effects/shipbonusrole4nosferatucpubonus.py @@ -0,0 +1,10 @@ +# shipBonusRole4NosferatuCPUBonus +# +# Used by: +# Ship: Dagon +# Ship: Rabisu +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Energy Nosferatu", "cpu", src.getModifiedItemAttr("shipBonusRole4")) diff --git a/eos/effects/shipbonusrole5capitalremotearmorrepairpowergridbonus.py b/eos/effects/shipbonusrole5capitalremotearmorrepairpowergridbonus.py new file mode 100644 index 000000000..8e33eb7a0 --- /dev/null +++ b/eos/effects/shipbonusrole5capitalremotearmorrepairpowergridbonus.py @@ -0,0 +1,9 @@ +# shipBonusRole5CapitalRemoteArmorRepairPowergridBonus +# +# Used by: +# Ship: Dagon +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capital Remote Armor Repair Systems"), "power", src.getModifiedItemAttr("shipBonusRole5")) diff --git a/eos/effects/remotearmorpowerneedbonuseffect.py b/eos/effects/shipbonusrole5remotearmorrepairpowergridbonus.py similarity index 63% rename from eos/effects/remotearmorpowerneedbonuseffect.py rename to eos/effects/shipbonusrole5remotearmorrepairpowergridbonus.py index f8f7b1661..3f764d908 100644 --- a/eos/effects/remotearmorpowerneedbonuseffect.py +++ b/eos/effects/shipbonusrole5remotearmorrepairpowergridbonus.py @@ -1,4 +1,4 @@ -# remoteArmorPowerNeedBonusEffect +# shipBonusRole5RemoteArmorRepairPowergridBonus # # Used by: # Ships from group: Logistics (3 of 6) @@ -7,4 +7,4 @@ type = "passive" def handler(fit, src, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Remote Armor Repair Systems"), "power", - src.getModifiedItemAttr("remoteArmorPowerNeedBonus")) + src.getModifiedItemAttr("shipBonusRole5")) diff --git a/eos/effects/shipbonussentrydronearmorhppiratefaction.py b/eos/effects/shipbonussentrydronearmorhppiratefaction.py index b2cf85c49..a1f49486f 100644 --- a/eos/effects/shipbonussentrydronearmorhppiratefaction.py +++ b/eos/effects/shipbonussentrydronearmorhppiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Sentry Drone Interfacing"), - "armorHP", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "armorHP", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonussentrydronedamagemultiplierpiratefaction.py b/eos/effects/shipbonussentrydronedamagemultiplierpiratefaction.py index 27da3b576..f218f02ed 100644 --- a/eos/effects/shipbonussentrydronedamagemultiplierpiratefaction.py +++ b/eos/effects/shipbonussentrydronedamagemultiplierpiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Sentry Drone Interfacing"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonussentrydronehppiratefaction.py b/eos/effects/shipbonussentrydronehppiratefaction.py index 0399ccfc8..fab5592b9 100644 --- a/eos/effects/shipbonussentrydronehppiratefaction.py +++ b/eos/effects/shipbonussentrydronehppiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Sentry Drone Interfacing"), - "hp", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "hp", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonussentrydroneshieldhppiratefaction.py b/eos/effects/shipbonussentrydroneshieldhppiratefaction.py index d69820fca..42db4065f 100644 --- a/eos/effects/shipbonussentrydroneshieldhppiratefaction.py +++ b/eos/effects/shipbonussentrydroneshieldhppiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.drones.filteredItemBoost(lambda drone: drone.item.requiresSkill("Sentry Drone Interfacing"), - "shieldCapacity", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "shieldCapacity", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonussmallenergyturretdamagepiratefaction.py b/eos/effects/shipbonussmallenergyturretdamagepiratefaction.py index 76eedd0b3..f4b2ccfca 100644 --- a/eos/effects/shipbonussmallenergyturretdamagepiratefaction.py +++ b/eos/effects/shipbonussmallenergyturretdamagepiratefaction.py @@ -12,4 +12,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Energy Turret"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipbonussmallmissileflighttimecf1.py b/eos/effects/shipbonussmallmissileflighttimecf1.py new file mode 100644 index 000000000..0b41b4185 --- /dev/null +++ b/eos/effects/shipbonussmallmissileflighttimecf1.py @@ -0,0 +1,12 @@ +# shipBonusSmallMissileFlightTimeCF1 +# +# Used by: +# Ship: Pacifier +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Rockets"), + "explosionDelay", src.getModifiedItemAttr("shipBonusCF"), skill="Caldari Frigate") + fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Light Missiles"), + "explosionDelay", src.getModifiedItemAttr("shipBonusCF"), skill="Caldari Frigate") diff --git a/eos/effects/shipmissilespeedbonuscf.py b/eos/effects/shipbonussmallmissilerofcf2.py similarity index 86% rename from eos/effects/shipmissilespeedbonuscf.py rename to eos/effects/shipbonussmallmissilerofcf2.py index c73d8a7db..9849ecc4b 100644 --- a/eos/effects/shipmissilespeedbonuscf.py +++ b/eos/effects/shipbonussmallmissilerofcf2.py @@ -1,8 +1,9 @@ -# shipMissileSpeedBonusCF +# shipBonusSmallMissileRoFCF2 # # Used by: # Ship: Buzzard # Ship: Hawk +# Ship: Pacifier type = "passive" diff --git a/eos/effects/shipbonussptfalloffmf2.py b/eos/effects/shipbonussptfalloffmf2.py index 9ec05f2c4..abc07cbca 100644 --- a/eos/effects/shipbonussptfalloffmf2.py +++ b/eos/effects/shipbonussptfalloffmf2.py @@ -1,6 +1,7 @@ # shipBonusSPTFalloffMF2 # # Used by: +# Ship: Pacifier # Ship: Rifter type = "passive" diff --git a/eos/effects/shipbonussptrofmf.py b/eos/effects/shipbonussptrofmf.py new file mode 100644 index 000000000..86ac5f332 --- /dev/null +++ b/eos/effects/shipbonussptrofmf.py @@ -0,0 +1,10 @@ +# shipBonusSPTRoFMF +# +# Used by: +# Ship: Pacifier +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Projectile Turret"), + "speed", src.getModifiedItemAttr("shipBonusMF"), skill="Minmatar Frigate") diff --git a/eos/effects/shipbonussurveyprobeexplosiondelayskillsurveycovertops3.py b/eos/effects/shipbonussurveyprobeexplosiondelayskillsurveycovertops3.py index 47e1b063b..08f67f3ab 100644 --- a/eos/effects/shipbonussurveyprobeexplosiondelayskillsurveycovertops3.py +++ b/eos/effects/shipbonussurveyprobeexplosiondelayskillsurveycovertops3.py @@ -1,7 +1,7 @@ # shipBonusSurveyProbeExplosionDelaySkillSurveyCovertOps3 # # Used by: -# Ships from group: Covert Ops (4 of 6) +# Ships from group: Covert Ops (5 of 7) type = "passive" diff --git a/eos/effects/shipbonustitana1energywarfareamountbonus.py b/eos/effects/shipbonustitana1energywarfareamountbonus.py new file mode 100644 index 000000000..596439814 --- /dev/null +++ b/eos/effects/shipbonustitana1energywarfareamountbonus.py @@ -0,0 +1,12 @@ +# shipBonusTitanA1EnergyWarfareAmountBonus +# +# Used by: +# Ship: Molok +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Energy Nosferatu", + "powerTransferAmount", src.getModifiedItemAttr("shipBonusTitanA1"), skill="Amarr Titan") + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Energy Neutralizer", + "energyNeutralizerAmount", src.getModifiedItemAttr("shipBonusTitanA1"), skill="Amarr Titan") diff --git a/eos/effects/shipbonustitana3warpstrength.py b/eos/effects/shipbonustitana3warpstrength.py index c7c5bfca5..e5fee5ffc 100644 --- a/eos/effects/shipbonustitana3warpstrength.py +++ b/eos/effects/shipbonustitana3warpstrength.py @@ -2,6 +2,7 @@ # # Used by: # Ship: Avatar +# Ship: Molok type = "passive" diff --git a/eos/effects/shipbonustitanm1webrangebonus.py b/eos/effects/shipbonustitanm1webrangebonus.py new file mode 100644 index 000000000..c026e376e --- /dev/null +++ b/eos/effects/shipbonustitanm1webrangebonus.py @@ -0,0 +1,10 @@ +# shipBonusTitanM1WebRangeBonus +# +# Used by: +# Ship: Molok +type = "passive" + + +def handler(fit, src, context): + fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Stasis Web", + "maxRange", src.getModifiedItemAttr("shipBonusTitanM1"), skill="Minmatar Titan") diff --git a/eos/effects/shipbonustitanm1webbonus.py b/eos/effects/shipbonustitanm1webstrengthbonus.py similarity index 89% rename from eos/effects/shipbonustitanm1webbonus.py rename to eos/effects/shipbonustitanm1webstrengthbonus.py index 6abfca9dd..54ba011c7 100644 --- a/eos/effects/shipbonustitanm1webbonus.py +++ b/eos/effects/shipbonustitanm1webstrengthbonus.py @@ -1,4 +1,4 @@ -# shipBonusTitanM1WebBonus +# shipBonusTitanM1WebStrengthBonus # # Used by: # Ship: Vanquisher diff --git a/eos/effects/shipbonustitanm3warpstrength.py b/eos/effects/shipbonustitanm3warpstrength.py index de59fc61e..7fba2f0da 100644 --- a/eos/effects/shipbonustitanm3warpstrength.py +++ b/eos/effects/shipbonustitanm3warpstrength.py @@ -1,8 +1,7 @@ # shipBonusTitanM3WarpStrength # # Used by: -# Ship: Ragnarok -# Ship: Vanquisher +# Ships from group: Titan (3 of 6) type = "passive" diff --git a/eos/effects/shipcapitalagilitybonus.py b/eos/effects/shipcapitalagilitybonus.py index 71c8e0ca4..4313eb4b4 100644 --- a/eos/effects/shipcapitalagilitybonus.py +++ b/eos/effects/shipcapitalagilitybonus.py @@ -1,12 +1,7 @@ # shipCapitalAgilityBonus # # Used by: -# Ships from group: Carrier (4 of 4) -# Ships from group: Dreadnought (5 of 5) -# Ships from group: Force Auxiliary (4 of 4) -# Ships from group: Supercarrier (6 of 6) -# Ships from group: Titan (5 of 5) -# Ship: Rorqual +# Items from market group: Ships > Capital Ships (28 of 37) type = "passive" diff --git a/eos/effects/shipetdamageaf.py b/eos/effects/shipetdamageaf.py index bc11bd6b9..939e163fa 100644 --- a/eos/effects/shipetdamageaf.py +++ b/eos/effects/shipetdamageaf.py @@ -4,6 +4,7 @@ # Ship: Crucifier Navy Issue # Ship: Crusader # Ship: Imperial Navy Slicer +# Ship: Pacifier type = "passive" diff --git a/eos/effects/shipetoptimalrange2af.py b/eos/effects/shipetoptimalrange2af.py index 49602fd47..99b829ab7 100644 --- a/eos/effects/shipetoptimalrange2af.py +++ b/eos/effects/shipetoptimalrange2af.py @@ -2,6 +2,7 @@ # # Used by: # Ship: Imperial Navy Slicer +# Ship: Pacifier type = "passive" diff --git a/eos/effects/shipheavyassaultmissileemdmgpiratecruiser.py b/eos/effects/shipheavyassaultmissileemdmgpiratecruiser.py index 50aecade1..4aff9d4a1 100644 --- a/eos/effects/shipheavyassaultmissileemdmgpiratecruiser.py +++ b/eos/effects/shipheavyassaultmissileemdmgpiratecruiser.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Assault Missiles"), - "emDamage", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "emDamage", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipheavyassaultmissileexpdmgpiratecruiser.py b/eos/effects/shipheavyassaultmissileexpdmgpiratecruiser.py index 089ec066b..be21d8f51 100644 --- a/eos/effects/shipheavyassaultmissileexpdmgpiratecruiser.py +++ b/eos/effects/shipheavyassaultmissileexpdmgpiratecruiser.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Assault Missiles"), - "explosiveDamage", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "explosiveDamage", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipheavyassaultmissilekindmgpiratecruiser.py b/eos/effects/shipheavyassaultmissilekindmgpiratecruiser.py index 81706525a..bbc9b2a43 100644 --- a/eos/effects/shipheavyassaultmissilekindmgpiratecruiser.py +++ b/eos/effects/shipheavyassaultmissilekindmgpiratecruiser.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Assault Missiles"), - "kineticDamage", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "kineticDamage", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipheavyassaultmissilethermdmgpiratecruiser.py b/eos/effects/shipheavyassaultmissilethermdmgpiratecruiser.py index 15d354e63..928d0a52c 100644 --- a/eos/effects/shipheavyassaultmissilethermdmgpiratecruiser.py +++ b/eos/effects/shipheavyassaultmissilethermdmgpiratecruiser.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Assault Missiles"), - "thermalDamage", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "thermalDamage", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipheavymissileemdmgpiratecruiser.py b/eos/effects/shipheavymissileemdmgpiratecruiser.py index 567d8a110..2b3003818 100644 --- a/eos/effects/shipheavymissileemdmgpiratecruiser.py +++ b/eos/effects/shipheavymissileemdmgpiratecruiser.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Missiles"), - "emDamage", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "emDamage", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipheavymissileexpdmgpiratecruiser.py b/eos/effects/shipheavymissileexpdmgpiratecruiser.py index b2f3179cb..73ca9bf7f 100644 --- a/eos/effects/shipheavymissileexpdmgpiratecruiser.py +++ b/eos/effects/shipheavymissileexpdmgpiratecruiser.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Missiles"), - "explosiveDamage", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "explosiveDamage", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipheavymissilekindmgpiratecruiser.py b/eos/effects/shipheavymissilekindmgpiratecruiser.py index 0d3543ddd..e1efd07ec 100644 --- a/eos/effects/shipheavymissilekindmgpiratecruiser.py +++ b/eos/effects/shipheavymissilekindmgpiratecruiser.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Missiles"), - "kineticDamage", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "kineticDamage", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipheavymissilethermdmgpiratecruiser.py b/eos/effects/shipheavymissilethermdmgpiratecruiser.py index f50b05771..28990be6a 100644 --- a/eos/effects/shipheavymissilethermdmgpiratecruiser.py +++ b/eos/effects/shipheavymissilethermdmgpiratecruiser.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Heavy Missiles"), - "thermalDamage", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "thermalDamage", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shiphtdmgbonusfixedgc.py b/eos/effects/shiphtdmgbonusfixedgc.py index 39ebd7b33..e6dfd16a2 100644 --- a/eos/effects/shiphtdmgbonusfixedgc.py +++ b/eos/effects/shiphtdmgbonusfixedgc.py @@ -4,6 +4,7 @@ # Ship: Adrestia # Ship: Arazu # Ship: Deimos +# Ship: Enforcer # Ship: Exequror Navy Issue # Ship: Guardian-Vexor # Ship: Thorax diff --git a/eos/effects/shiphybriddmgpiratebattleship.py b/eos/effects/shiphybriddmgpiratebattleship.py index dd959b187..417ed58aa 100644 --- a/eos/effects/shiphybriddmgpiratebattleship.py +++ b/eos/effects/shiphybriddmgpiratebattleship.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Large Hybrid Turret"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shiphybriddmgpiratecruiser.py b/eos/effects/shiphybriddmgpiratecruiser.py index b0987b4b4..7d01b20c6 100644 --- a/eos/effects/shiphybriddmgpiratecruiser.py +++ b/eos/effects/shiphybriddmgpiratecruiser.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Medium Hybrid Turret"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shiphybridtrackinggc2.py b/eos/effects/shiphybridtrackinggc2.py index 5f2eac55a..4169cca0d 100644 --- a/eos/effects/shiphybridtrackinggc2.py +++ b/eos/effects/shiphybridtrackinggc2.py @@ -1,6 +1,7 @@ # shipHybridTrackingGC2 # # Used by: +# Ship: Enforcer # Ship: Thorax type = "passive" diff --git a/eos/effects/shiplaserdamagepiratebattleship.py b/eos/effects/shiplaserdamagepiratebattleship.py index 7625b2383..ff1e9558a 100644 --- a/eos/effects/shiplaserdamagepiratebattleship.py +++ b/eos/effects/shiplaserdamagepiratebattleship.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Large Energy Turret"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipmetcdamagebonusac.py b/eos/effects/shipmetcdamagebonusac.py index 485d71457..8cdb47763 100644 --- a/eos/effects/shipmetcdamagebonusac.py +++ b/eos/effects/shipmetcdamagebonusac.py @@ -2,6 +2,7 @@ # # Used by: # Ship: Augoror Navy Issue +# Ship: Enforcer # Ship: Maller # Ship: Omen Navy Issue type = "passive" diff --git a/eos/effects/shipmissilerofcc.py b/eos/effects/shipmissilerofcc.py index 785726f9c..bda638ace 100644 --- a/eos/effects/shipmissilerofcc.py +++ b/eos/effects/shipmissilerofcc.py @@ -2,6 +2,7 @@ # # Used by: # Ships named like: Caracal (2 of 2) +# Ship: Enforcer type = "passive" diff --git a/eos/effects/shipmissilevelocitypiratefactionfrigate.py b/eos/effects/shipmissilevelocitypiratefactionfrigate.py index 4e4e03685..16c3df09c 100644 --- a/eos/effects/shipmissilevelocitypiratefactionfrigate.py +++ b/eos/effects/shipmissilevelocitypiratefactionfrigate.py @@ -9,4 +9,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), - "maxVelocity", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "maxVelocity", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipmissilevelocitypiratefactionlight.py b/eos/effects/shipmissilevelocitypiratefactionlight.py index ece99d75b..1cea02ec3 100644 --- a/eos/effects/shipmissilevelocitypiratefactionlight.py +++ b/eos/effects/shipmissilevelocitypiratefactionlight.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Light Missiles"), - "maxVelocity", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "maxVelocity", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipmissilevelocitypiratefactionrocket.py b/eos/effects/shipmissilevelocitypiratefactionrocket.py index 23cf6f34b..cc6e55c5d 100644 --- a/eos/effects/shipmissilevelocitypiratefactionrocket.py +++ b/eos/effects/shipmissilevelocitypiratefactionrocket.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Rockets"), - "maxVelocity", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "maxVelocity", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipmoduleguidancedisruptor.py b/eos/effects/shipmoduleguidancedisruptor.py index 1588d52d7..a48eb6e78 100644 --- a/eos/effects/shipmoduleguidancedisruptor.py +++ b/eos/effects/shipmoduleguidancedisruptor.py @@ -5,7 +5,7 @@ type = "active", "projected" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" in context: for srcAttr, tgtAttr in ( ("aoeCloudSizeBonus", "aoeCloudSize"), @@ -15,4 +15,4 @@ def handler(fit, module, context): ): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), tgtAttr, module.getModifiedItemAttr(srcAttr), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/shipmoduleremotetrackingcomputer.py b/eos/effects/shipmoduleremotetrackingcomputer.py index 83a63b97f..698307183 100644 --- a/eos/effects/shipmoduleremotetrackingcomputer.py +++ b/eos/effects/shipmoduleremotetrackingcomputer.py @@ -5,14 +5,14 @@ type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, **kwargs): if "projected" in context: fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "trackingSpeed", module.getModifiedItemAttr("trackingSpeedBonus"), - stackingPenalties=True) + stackingPenalties=True, **kwargs) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "maxRange", module.getModifiedItemAttr("maxRangeBonus"), - stackingPenalties=True) + stackingPenalties=True, **kwargs) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "falloff", module.getModifiedItemAttr("falloffBonus"), - stackingPenalties=True) + stackingPenalties=True, **kwargs) diff --git a/eos/effects/shipmoduletrackingdisruptor.py b/eos/effects/shipmoduletrackingdisruptor.py index e970dae3c..2c04a4a6e 100644 --- a/eos/effects/shipmoduletrackingdisruptor.py +++ b/eos/effects/shipmoduletrackingdisruptor.py @@ -5,14 +5,14 @@ type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" in context: fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "trackingSpeed", module.getModifiedItemAttr("trackingSpeedBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "maxRange", module.getModifiedItemAttr("maxRangeBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "falloff", module.getModifiedItemAttr("falloffBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/shipprojectiledmgpiratecruiser.py b/eos/effects/shipprojectiledmgpiratecruiser.py index 6753262b9..f8c60bd63 100644 --- a/eos/effects/shipprojectiledmgpiratecruiser.py +++ b/eos/effects/shipprojectiledmgpiratecruiser.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Medium Projectile Turret"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipprojectilerofpiratebattleship.py b/eos/effects/shipprojectilerofpiratebattleship.py index dac3c2544..9b0a7168f 100644 --- a/eos/effects/shipprojectilerofpiratebattleship.py +++ b/eos/effects/shipprojectilerofpiratebattleship.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Large Projectile Turret"), - "speed", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "speed", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipprojectilerofpiratecruiser.py b/eos/effects/shipprojectilerofpiratecruiser.py index fae33e206..25a500674 100644 --- a/eos/effects/shipprojectilerofpiratecruiser.py +++ b/eos/effects/shipprojectilerofpiratecruiser.py @@ -8,4 +8,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Medium Projectile Turret"), - "speed", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "speed", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shippturretfalloffbonusmc2.py b/eos/effects/shippturretfalloffbonusmc2.py index b47f36f1d..85f4a70f8 100644 --- a/eos/effects/shippturretfalloffbonusmc2.py +++ b/eos/effects/shippturretfalloffbonusmc2.py @@ -1,6 +1,7 @@ # shipPTurretFalloffBonusMC2 # # Used by: +# Ship: Enforcer # Ship: Stabber type = "passive" diff --git a/eos/effects/shippturretspeedbonusmc.py b/eos/effects/shippturretspeedbonusmc.py index 713a2b7de..1f360a87a 100644 --- a/eos/effects/shippturretspeedbonusmc.py +++ b/eos/effects/shippturretspeedbonusmc.py @@ -3,6 +3,7 @@ # Used by: # Variations of ship: Rupture (3 of 3) # Variations of ship: Stabber (3 of 3) +# Ship: Enforcer # Ship: Huginn # Ship: Scythe Fleet Issue type = "passive" diff --git a/eos/effects/shipscanprobestrengthbonuspiratefaction.py b/eos/effects/shipscanprobestrengthbonuspiratefaction.py index c5146f252..26ccc7c2c 100644 --- a/eos/effects/shipscanprobestrengthbonuspiratefaction.py +++ b/eos/effects/shipscanprobestrengthbonuspiratefaction.py @@ -7,4 +7,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Astrometrics"), - "baseSensorStrength", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "baseSensorStrength", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipshtdmgbonusgf.py b/eos/effects/shipshtdmgbonusgf.py index 3162880ba..d99426da0 100644 --- a/eos/effects/shipshtdmgbonusgf.py +++ b/eos/effects/shipshtdmgbonusgf.py @@ -5,6 +5,7 @@ # Ship: Atron # Ship: Federation Navy Comet # Ship: Helios +# Ship: Pacifier # Ship: Taranis type = "passive" diff --git a/eos/effects/shipsmallmissiledmgpiratefaction.py b/eos/effects/shipsmallmissiledmgpiratefaction.py index db9e10202..3a21076cc 100644 --- a/eos/effects/shipsmallmissiledmgpiratefaction.py +++ b/eos/effects/shipsmallmissiledmgpiratefaction.py @@ -10,4 +10,4 @@ def handler(fit, ship, context): for damageType in ("em", "explosive", "kinetic", "thermal"): fit.modules.filteredChargeBoost( lambda mod: mod.charge.requiresSkill("Rockets") or mod.charge.requiresSkill("Light Missiles"), - "{0}Damage".format(damageType), ship.getModifiedItemAttr("shipBonusPirateFaction")) + "{0}Damage".format(damageType), ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/shipxlprojectiledamagerole.py b/eos/effects/shipxlprojectiledamagerole.py index 62f670f18..eaa09058e 100644 --- a/eos/effects/shipxlprojectiledamagerole.py +++ b/eos/effects/shipxlprojectiledamagerole.py @@ -4,4 +4,4 @@ type = "passive" def handler(fit, ship, context): fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capital Projectile Turret"), - "damageMultiplier", ship.getModifiedItemAttr("shipBonusPirateFaction")) + "damageMultiplier", ship.getModifiedItemAttr("shipBonusRole7")) diff --git a/eos/effects/structuremoduleeffectremotesensordampener.py b/eos/effects/structuremoduleeffectremotesensordampener.py index 7260ef623..08aa61e57 100644 --- a/eos/effects/structuremoduleeffectremotesensordampener.py +++ b/eos/effects/structuremoduleeffectremotesensordampener.py @@ -3,12 +3,12 @@ type = "projected", "active" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" not in context: return fit.ship.boostItemAttr("maxTargetRange", module.getModifiedItemAttr("maxTargetRangeBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.ship.boostItemAttr("scanResolution", module.getModifiedItemAttr("scanResolutionBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/structuremoduleeffectstasiswebifier.py b/eos/effects/structuremoduleeffectstasiswebifier.py index 0aa907fc6..aa0b0d553 100644 --- a/eos/effects/structuremoduleeffectstasiswebifier.py +++ b/eos/effects/structuremoduleeffectstasiswebifier.py @@ -2,8 +2,8 @@ type = "active", "projected" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" not in context: return fit.ship.boostItemAttr("maxVelocity", module.getModifiedItemAttr("speedFactor"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/structuremoduleeffecttargetpainter.py b/eos/effects/structuremoduleeffecttargetpainter.py index aac9005ba..7c15140e3 100644 --- a/eos/effects/structuremoduleeffecttargetpainter.py +++ b/eos/effects/structuremoduleeffecttargetpainter.py @@ -2,7 +2,7 @@ type = "projected", "active" -def handler(fit, container, context): +def handler(fit, container, context, *args, **kwargs): if "projected" in context: fit.ship.boostItemAttr("signatureRadius", container.getModifiedItemAttr("signatureRadiusBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/structuremoduleeffectweapondisruption.py b/eos/effects/structuremoduleeffectweapondisruption.py index 82e67797e..3b6365e37 100644 --- a/eos/effects/structuremoduleeffectweapondisruption.py +++ b/eos/effects/structuremoduleeffectweapondisruption.py @@ -3,7 +3,7 @@ type = "active", "projected" -def handler(fit, module, context): +def handler(fit, module, context, *args, **kwargs): if "projected" in context: for srcAttr, tgtAttr in ( ("aoeCloudSizeBonus", "aoeCloudSize"), @@ -13,14 +13,14 @@ def handler(fit, module, context): ): fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), tgtAttr, module.getModifiedItemAttr(srcAttr), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "trackingSpeed", module.getModifiedItemAttr("trackingSpeedBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "maxRange", module.getModifiedItemAttr("maxRangeBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Gunnery"), "falloff", module.getModifiedItemAttr("falloffBonus"), - stackingPenalties=True, remoteResists=True) + stackingPenalties=True, *args, **kwargs) diff --git a/eos/effects/tractorbeamcan.py b/eos/effects/tractorbeamcan.py index 2c7f8eae3..293a839ea 100644 --- a/eos/effects/tractorbeamcan.py +++ b/eos/effects/tractorbeamcan.py @@ -2,10 +2,8 @@ # # Used by: # Modules from group: Tractor Beam (4 of 4) -from eos.config import settings type = "active" def handler(fit, module, context): - print settings['setting1'] pass diff --git a/eos/events.py b/eos/events.py new file mode 100644 index 000000000..de8bdfadb --- /dev/null +++ b/eos/events.py @@ -0,0 +1,87 @@ +# Decided to put this in it's own file so that we can easily choose not to import it (thanks to mac-deprecated builds =/) + +import datetime +from sqlalchemy.event import listen +from sqlalchemy.orm.collections import InstrumentedList + +from eos.db.saveddata.fit import projectedFitSourceRel, boostedOntoRel + +from eos.saveddata.fit import Fit +from eos.saveddata.module import Module +from eos.saveddata.drone import Drone +from eos.saveddata.fighter import Fighter +from eos.saveddata.cargo import Cargo +from eos.saveddata.implant import Implant +from eos.saveddata.booster import Booster + +ignored_rels = [ + projectedFitSourceRel, + boostedOntoRel +] + + +def update_fit_modified(target, value, oldvalue, initiator): + if not target.owner: + return + + if value != oldvalue: + # some things (like Implants) have a backref to the fit, which actually produces a list. + # In this situation, simply take the 0 index to get to the fit. + # There may be cases in the future in which there are multiple fits, so this should be + # looked at more indepth later + if isinstance(target.owner, InstrumentedList): + parent = target.owner[0] + else: + parent = target.owner + + # ensure this is a fit we're dealing with + if isinstance(parent, Fit): + parent.modified = datetime.datetime.now() + + +def apply_col_listeners(target, context): + # We only want to set these events when the module is first loaded (otherwise events will fire during the initial + # population of data). This runs through all columns and sets up "set" events on each column. We do it with each + # column because the alternative would be to do a before/after_update for the Mapper itself, however we're only + # allowed to change the local attributes during those events as that's inter-flush. + # See http://docs.sqlalchemy.org/en/rel_1_0/orm/session_events.html#mapper-level-events + + # @todo replace with `inspect(Module).column_attrs` when mac binaries are updated + + manager = getattr(target.__class__, "_sa_class_manager", None) + if manager: + for col in manager.mapper.column_attrs: + listen(col, 'set', update_fit_modified) + + +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) + target.modified = datetime.datetime.now() + + +def apply_rel_listeners(target, context): + # We only want to see these events when the fit is first loaded (otherwise events will fire during the initial + # population of data). This sets listeners for all the relationships on fits. This allows us to update the fit's + # modified date whenever something is added/removed from fit + # See http://docs.sqlalchemy.org/en/rel_1_0/orm/events.html#sqlalchemy.orm.events.InstanceEvents.load + + # todo: when we can, move over to `inspect(es_Fit).relationships` (when mac binaries are updated) + manager = getattr(target.__class__, "_sa_class_manager", None) + if manager: + for rel in manager.mapper.relationships: + if rel in ignored_rels: + continue + listen(rel, 'append', rel_listener) + listen(rel, 'remove', rel_listener) + + +listen(Fit, 'load', apply_rel_listeners) +listen(Module, 'load', apply_col_listeners) +listen(Drone, 'load', apply_col_listeners) +listen(Fighter, 'load', apply_col_listeners) +listen(Cargo, 'load', apply_col_listeners) +listen(Implant, 'load', apply_col_listeners) +listen(Booster, 'load', apply_col_listeners) diff --git a/eos/gamedata.py b/eos/gamedata.py index 6b7aae34f..cf5fe1777 100644 --- a/eos/gamedata.py +++ b/eos/gamedata.py @@ -23,6 +23,7 @@ from sqlalchemy.orm import reconstructor import eos.db from eqBase import EqBase +from eos.saveddata.price import Price as types_Price try: from collections import OrderedDict @@ -159,6 +160,9 @@ class Effect(EqBase): Grab the handler, type and runTime from the effect code if it exists, if it doesn't, set dummy values and add a dummy handler """ + + pyfalog.debug("Generate effect handler for {}".format(self.name)) + try: self.__effectModule = effectModule = __import__('eos.effects.' + self.handlerName, fromlist=True) self.__handler = getattr(effectModule, "handler", effectDummy) @@ -174,7 +178,7 @@ class Effect(EqBase): self.__runTime = "normal" self.__activeByDefault = True self.__type = None - pyfalog.warning("ImportError generating handler: {0}", e) + pyfalog.debug("ImportError generating handler: {0}", e) except (AttributeError) as e: # Effect probably exists but there is an issue with it. Turn it into a dummy effect so we can continue, but flag it with an error. self.__handler = effectDummy @@ -183,6 +187,10 @@ class Effect(EqBase): self.__type = None pyfalog.error("AttributeError generating handler: {0}", e) except Exception as e: + self.__handler = effectDummy + self.__runTime = "normal" + self.__activeByDefault = True + self.__type = None pyfalog.critical("Exception generating handler:") pyfalog.critical(e) @@ -230,10 +238,12 @@ class Item(EqBase): def init(self): self.__race = None self.__requiredSkills = None + self.__requiredFor = None self.__moved = False self.__offensive = None self.__assistive = None self.__overrides = None + self.__price = None @property def attributes(self): @@ -281,6 +291,8 @@ class Item(EqBase): eos.db.saveddata_session.delete(override) eos.db.commit() + srqIDMap = {182: 277, 183: 278, 184: 279, 1285: 1286, 1289: 1287, 1290: 1288} + @property def requiredSkills(self): if self.__requiredSkills is None: @@ -288,8 +300,7 @@ class Item(EqBase): self.__requiredSkills = requiredSkills # Map containing attribute IDs we may need for required skills # { requiredSkillX : requiredSkillXLevel } - srqIDMap = {182: 277, 183: 278, 184: 279, 1285: 1286, 1289: 1287, 1290: 1288} - combinedAttrIDs = set(srqIDMap.iterkeys()).union(set(srqIDMap.itervalues())) + combinedAttrIDs = set(self.srqIDMap.iterkeys()).union(set(self.srqIDMap.itervalues())) # Map containing result of the request # { attributeID : attributeValue } skillAttrs = {} @@ -299,7 +310,7 @@ class Item(EqBase): attrVal = attrInfo[2] skillAttrs[attrID] = attrVal # Go through all attributeID pairs - for srqIDAtrr, srqLvlAttr in srqIDMap.iteritems(): + for srqIDAtrr, srqLvlAttr in self.srqIDMap.iteritems(): # Check if we have both in returned result if srqIDAtrr in skillAttrs and srqLvlAttr in skillAttrs: skillID = int(skillAttrs[srqIDAtrr]) @@ -309,6 +320,23 @@ class Item(EqBase): requiredSkills[item] = skillLvl return self.__requiredSkills + @property + def requiredFor(self): + if self.__requiredFor is None: + self.__requiredFor = dict() + + # Map containing attribute IDs we may need for required skills + + # Get relevant attribute values from db (required skill IDs and levels) for our item + q = eos.db.getRequiredFor(self.ID, self.srqIDMap) + + for itemID, lvl in q: + # Fetch item from database and fill map + item = eos.db.getItem(itemID) + self.__requiredFor[item] = lvl + + return self.__requiredFor + factionMap = { 500001: "caldari", 500002: "minmatar", @@ -379,7 +407,7 @@ class Item(EqBase): assistive = False # Go through all effects and find first assistive for effect in self.effects.itervalues(): - if effect.info.isAssistance is True: + if effect.isAssistance is True: # If we find one, stop and mark item as assistive assistive = True break @@ -394,7 +422,7 @@ class Item(EqBase): offensive = False # Go through all effects and find first offensive for effect in self.effects.itervalues(): - if effect.info.isOffensive is True: + if effect.isOffensive is True: # If we find one, stop and mark item as offensive offensive = True break @@ -419,8 +447,29 @@ class Item(EqBase): return False + @property + def price(self): + + # todo: use `from sqlalchemy import inspect` instead (mac-deprecated doesn't have inspect(), was imp[lemented in 0.8) + if self.__price is not None and getattr(self.__price, '_sa_instance_state', None) and self.__price._sa_instance_state.deleted: + pyfalog.debug("Price data for {} was deleted (probably from a cache reset), resetting object".format(self.ID)) + self.__price = None + + if self.__price is None: + db_price = eos.db.getPrice(self.ID) + # do not yet have a price in the database for this item, create one + if db_price is None: + pyfalog.debug("Creating a price for {}".format(self.ID)) + self.__price = types_Price(self.ID) + eos.db.add(self.__price) + eos.db.commit() + else: + self.__price = db_price + + return self.__price + def __repr__(self): - return "Item(ID={}, name={}) at {}".format( + return u"Item(ID={}, name={}) at {}".format( self.ID, self.name, hex(id(self)) ) @@ -429,7 +478,7 @@ class MetaData(EqBase): pass -class EffectInfo(EqBase): +class ItemEffect(EqBase): pass diff --git a/eos/graph/fitDps.py b/eos/graph/fitDps.py index f09a2c34b..cfa70187a 100644 --- a/eos/graph/fitDps.py +++ b/eos/graph/fitDps.py @@ -27,10 +27,12 @@ pyfalog = Logger(__name__) class FitDpsGraph(Graph): - defaults = {"angle": 0, - "distance": 0, - "signatureRadius": None, - "velocity": 0} + defaults = { + "angle" : 0, + "distance" : 0, + "signatureRadius": None, + "velocity" : 0 + } def __init__(self, fit, data=None): Graph.__init__(self, fit, self.calcDps, data if data is not None else self.defaults) @@ -47,16 +49,16 @@ class FitDpsGraph(Graph): if not mod.isEmpty and mod.state >= State.ACTIVE: if "remoteTargetPaintFalloff" in mod.item.effects: ew['signatureRadius'].append( - 1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) * self.calculateModuleMultiplier( - mod, data)) + 1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) * self.calculateModuleMultiplier( + mod, data)) if "remoteWebifierFalloff" in mod.item.effects: if distance <= mod.getModifiedItemAttr("maxRange"): ew['velocity'].append(1 + (mod.getModifiedItemAttr("speedFactor") / 100)) elif mod.getModifiedItemAttr("falloffEffectiveness") > 0: # I am affected by falloff ew['velocity'].append( - 1 + (mod.getModifiedItemAttr("speedFactor") / 100) * self.calculateModuleMultiplier(mod, - data)) + 1 + (mod.getModifiedItemAttr("speedFactor") / 100) * self.calculateModuleMultiplier(mod, + data)) ew['signatureRadius'].sort(key=abssort) ew['velocity'].sort(key=abssort) @@ -85,7 +87,7 @@ class FitDpsGraph(Graph): if distance <= fit.extraAttributes["droneControlRange"]: for drone in fit.drones: multiplier = 1 if drone.getModifiedItemAttr("maxVelocity") > 1 else self.calculateTurretMultiplier( - drone, data) + drone, data) dps, _ = drone.damageStats(fit.targetResists) total += dps * multiplier @@ -149,7 +151,7 @@ class FitDpsGraph(Graph): damageReductionSensitivity = ability.fighter.getModifiedItemAttr("{}ReductionSensitivity".format(prefix)) if damageReductionSensitivity is None: damageReductionSensitivity = ability.fighter.getModifiedItemAttr( - "{}DamageReductionSensitivity".format(prefix)) + "{}DamageReductionSensitivity".format(prefix)) targetSigRad = explosionRadius if targetSigRad is None else targetSigRad sigRadiusFactor = targetSigRad / explosionRadius diff --git a/eos/modifiedAttributeDict.py b/eos/modifiedAttributeDict.py index 757d8a3d1..55463448c 100644 --- a/eos/modifiedAttributeDict.py +++ b/eos/modifiedAttributeDict.py @@ -19,6 +19,9 @@ import collections from math import exp +# TODO: This needs to be moved out, we shouldn't have *ANY* dependencies back to other modules/methods inside eos. +# This also breaks writing any tests. :( +from eos.db.gamedata.queries import getAttributeInfo defaultValuesCache = {} cappingAttrKeyCache = {} @@ -26,22 +29,26 @@ cappingAttrKeyCache = {} class ItemAttrShortcut(object): def getModifiedItemAttr(self, key, default=None): - if key in self.itemModifiedAttributes: - return self.itemModifiedAttributes[key] - else: - return default + return_value = self.itemModifiedAttributes.get(key) + + if return_value is None and default is not None: + return_value = default + + return return_value class ChargeAttrShortcut(object): def getModifiedChargeAttr(self, key, default=None): - if key in self.chargeModifiedAttributes: - return self.chargeModifiedAttributes[key] - else: - return default + return_value = self.chargeModifiedAttributes.get(key) + + if return_value is None and default is not None: + return_value = default + + return return_value class ModifiedAttributeDict(collections.MutableMapping): - OVERRIDES = False + overrides_enabled = False class CalculationPlaceholder(object): def __init__(self): @@ -98,15 +105,23 @@ class ModifiedAttributeDict(collections.MutableMapping): def __getitem__(self, key): # Check if we have final calculated value - if key in self.__modified: - if self.__modified[key] == self.CalculationPlaceholder: - self.__modified[key] = self.__calculateValue(key) - return self.__modified[key] + key_value = self.__modified.get(key) + if key_value is self.CalculationPlaceholder: + key_value = self.__modified[key] = self.__calculateValue(key) + + if key_value is not None: + return key_value + # Then in values which are not yet calculated - elif key in self.__intermediary: - return self.__intermediary[key] - # Original value is the least priority + if self.__intermediary: + val = self.__intermediary.get(key) else: + val = None + + if val is not None: + return val + else: + # Original value is the least priority return self.getOriginal(key) def __delitem__(self, key): @@ -115,12 +130,18 @@ class ModifiedAttributeDict(collections.MutableMapping): if key in self.__intermediary: del self.__intermediary[key] - def getOriginal(self, key): - if self.OVERRIDES and key in self.__overrides: - return self.__overrides.get(key).value - val = self.__original.get(key) + def getOriginal(self, key, default=None): + if self.overrides_enabled and self.overrides: + val = self.overrides.get(key, None) + else: + val = None + if val is None: - return None + if self.original: + val = self.original.get(key, None) + + if val is None and val != default: + val = default return val.value if hasattr(val, "value") else val @@ -128,12 +149,12 @@ class ModifiedAttributeDict(collections.MutableMapping): self.__intermediary[key] = val def __iter__(self): - all = dict(self.__original, **self.__modified) - return (key for key in all) + all_dict = dict(self.original, **self.__modified) + return (key for key in all_dict) def __contains__(self, key): - return (self.__original is not None and key in self.__original) or \ - key in self.__modified or key in self.__intermediary + return (self.original is not None and key in self.original) or \ + key in self.__modified or key in self.__intermediary def __placehold(self, key): """Create calculation placeholder in item's modified attribute dict""" @@ -141,7 +162,7 @@ class ModifiedAttributeDict(collections.MutableMapping): def __len__(self): keys = set() - keys.update(self.__original.iterkeys()) + keys.update(self.original.iterkeys()) keys.update(self.__modified.iterkeys()) keys.update(self.__intermediary.iterkeys()) return len(keys) @@ -152,7 +173,6 @@ class ModifiedAttributeDict(collections.MutableMapping): try: cappingKey = cappingAttrKeyCache[key] except KeyError: - from eos.db.gamedata.queries import getAttributeInfo attrInfo = getAttributeInfo(key) if attrInfo is None: cappingId = cappingAttrKeyCache[key] = None @@ -166,12 +186,8 @@ class ModifiedAttributeDict(collections.MutableMapping): cappingKey = None if cappingAttrInfo is None else cappingAttrInfo.name if cappingKey: - if cappingKey in self.original: - # some items come with their own caps (ie: carriers). If they do, use this - cappingValue = self.original.get(cappingKey).value - else: - # If not, get info about the default value - cappingValue = self.__calculateValue(cappingKey) + cappingValue = self.original.get(cappingKey, self.__calculateValue(cappingKey)) + cappingValue = cappingValue.value if hasattr(cappingValue, "value") else cappingValue else: cappingValue = None @@ -183,25 +199,28 @@ class ModifiedAttributeDict(collections.MutableMapping): force = min(force, cappingValue) return force # Grab our values if they're there, otherwise we'll take default values - preIncrease = self.__preIncreases[key] if key in self.__preIncreases else 0 - multiplier = self.__multipliers[key] if key in self.__multipliers else 1 - penalizedMultiplierGroups = self.__penalizedMultipliers[key] if key in self.__penalizedMultipliers else {} - postIncrease = self.__postIncreases[key] if key in self.__postIncreases else 0 + preIncrease = self.__preIncreases.get(key, 0) + multiplier = self.__multipliers.get(key, 1) + penalizedMultiplierGroups = self.__penalizedMultipliers.get(key, {}) + postIncrease = self.__postIncreases.get(key, 0) # Grab initial value, priorities are: # Results of ongoing calculation > preAssign > original > 0 try: default = defaultValuesCache[key] except KeyError: - from eos.db.gamedata.queries import getAttributeInfo attrInfo = getAttributeInfo(key) if attrInfo is None: default = defaultValuesCache[key] = 0.0 else: dv = attrInfo.defaultValue default = defaultValuesCache[key] = dv if dv is not None else 0.0 - val = self.__intermediary[key] if key in self.__intermediary else self.__preAssigns[ - key] if key in self.__preAssigns else self.getOriginal(key) if key in self.__original else default + + val = self.__intermediary.get(key, + self.__preAssigns.get(key, + self.getOriginal(key, default) + ) + ) # We'll do stuff in the following order: # preIncrease > multiplier > stacking penalized multipliers > postIncrease @@ -254,7 +273,7 @@ class ModifiedAttributeDict(collections.MutableMapping): return skill.level def getAfflictions(self, key): - return self.__affectedBy[key] if key in self.__affectedBy else {} + return self.__affectedBy.get(key, {}) def iterAfflictions(self): return self.__affectedBy.__iter__() @@ -287,11 +306,14 @@ class ModifiedAttributeDict(collections.MutableMapping): self.__placehold(attributeName) self.__afflict(attributeName, "=", value, value != self.getOriginal(attributeName)) - def increase(self, attributeName, increase, position="pre", skill=None): + def increase(self, attributeName, increase, position="pre", skill=None, **kwargs): """Increase value of given attribute by given number""" if skill: increase *= self.__handleSkill(skill) + if 'effect' in kwargs: + increase *= ModifiedAttributeDict.getResistance(self.fit, kwargs['effect']) or 1 + # Increases applied before multiplications and after them are # written in separate maps if position == "pre": @@ -306,7 +328,7 @@ class ModifiedAttributeDict(collections.MutableMapping): self.__placehold(attributeName) self.__afflict(attributeName, "+", increase, increase != 0) - def multiply(self, attributeName, multiplier, stackingPenalties=False, penaltyGroup="default", skill=None): + def multiply(self, attributeName, multiplier, stackingPenalties=False, penaltyGroup="default", skill=None, resist=True, *args, **kwargs): """Multiply value of given attribute by given factor""" if multiplier is None: # See GH issue 397 return @@ -330,27 +352,29 @@ class ModifiedAttributeDict(collections.MutableMapping): self.__multipliers[attributeName] *= multiplier self.__placehold(attributeName) - self.__afflict(attributeName, "%s*" % ("s" if stackingPenalties else ""), multiplier, multiplier != 1) - def boost(self, attributeName, boostFactor, skill=None, remoteResists=False, *args, **kwargs): + afflictPenal = "" + if stackingPenalties: + afflictPenal += "s" + if resist: + afflictPenal += "r" + + self.__afflict(attributeName, "%s*" % (afflictPenal), multiplier, multiplier != 1) + + def boost(self, attributeName, boostFactor, skill=None, *args, **kwargs): """Boost value by some percentage""" if skill: boostFactor *= self.__handleSkill(skill) - if remoteResists: - # @todo: this is such a disgusting hack. Look into sending these checks to the module class before the - # effect is applied. - mod = self.fit.getModifier() - remoteResistID = mod.getModifiedItemAttr("remoteResistanceID") or None + resist = None - # We really don't have a way of getting a ships attribute by ID. Fail. - resist = next((x for x in self.fit.ship.item.attributes.values() if x.ID == remoteResistID), None) - - if remoteResistID and resist: - boostFactor *= resist.value + # Goddammit CCP, make up your mind where you want this information >.< See #1139 + if 'effect' in kwargs: + resist = ModifiedAttributeDict.getResistance(self.fit, kwargs['effect']) or 1 + boostFactor *= resist # We just transform percentage boost into multiplication factor - self.multiply(attributeName, 1 + boostFactor / 100.0, *args, **kwargs) + self.multiply(attributeName, 1 + boostFactor / 100.0, resist=(True if resist else False), *args, **kwargs) def force(self, attributeName, value): """Force value to attribute and prohibit any changes to it""" @@ -358,8 +382,27 @@ class ModifiedAttributeDict(collections.MutableMapping): self.__placehold(attributeName) self.__afflict(attributeName, u"\u2263", value) + @staticmethod + def getResistance(fit, effect): + remoteResistID = effect.resistanceID + + # If it doesn't exist on the effect, check the modifying modules attributes. If it's there, set it on the + # effect for this session so that we don't have to look here again (won't always work when it's None, but + # will catch most) + if not remoteResistID: + mod = fit.getModifier() + effect.resistanceID = int(mod.getModifiedItemAttr("remoteResistanceID")) or None + remoteResistID = effect.resistanceID + + attrInfo = getAttributeInfo(remoteResistID) + + # Get the attribute of the resist + resist = fit.ship.itemModifiedAttributes[attrInfo.attributeName] or None + + return resist or 1.0 + class Affliction(object): - def __init__(self, type, amount): - self.type = type + def __init__(self, affliction_type, amount): + self.type = affliction_type self.amount = amount diff --git a/eos/saveddata/booster.py b/eos/saveddata/booster.py index 5ef2d78f6..62eb4d88f 100644 --- a/eos/saveddata/booster.py +++ b/eos/saveddata/booster.py @@ -133,11 +133,13 @@ class Booster(HandledItem, ItemAttrShortcut): @validates("ID", "itemID", "ammoID", "active") def validator(self, key, val): - map = {"ID": lambda _val: isinstance(_val, int), - "itemID": lambda _val: isinstance(_val, int), - "ammoID": lambda _val: isinstance(_val, int), - "active": lambda _val: isinstance(_val, bool), - "slot": lambda _val: isinstance(_val, int) and 1 <= _val <= 3} + map = { + "ID" : lambda _val: isinstance(_val, int), + "itemID": lambda _val: isinstance(_val, int), + "ammoID": lambda _val: isinstance(_val, int), + "active": lambda _val: isinstance(_val, bool), + "slot" : lambda _val: isinstance(_val, int) and 1 <= _val <= 3 + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) diff --git a/eos/saveddata/cargo.py b/eos/saveddata/cargo.py index 57da22e79..7b6e71349 100644 --- a/eos/saveddata/cargo.py +++ b/eos/saveddata/cargo.py @@ -71,9 +71,11 @@ class Cargo(HandledItem, ItemAttrShortcut): @validates("fitID", "itemID", "amount") def validator(self, key, val): - map = {"fitID": lambda _val: isinstance(_val, int), - "itemID": lambda _val: isinstance(_val, int), - "amount": lambda _val: isinstance(_val, int)} + map = { + "fitID" : lambda _val: isinstance(_val, int), + "itemID": lambda _val: isinstance(_val, int), + "amount": lambda _val: isinstance(_val, int) + } if key == "amount" and val > sys.maxint: val = sys.maxint diff --git a/eos/saveddata/character.py b/eos/saveddata/character.py index 067b9f98d..1c0c7cecf 100644 --- a/eos/saveddata/character.py +++ b/eos/saveddata/character.py @@ -17,6 +17,7 @@ # along with eos. If not, see . # =============================================================================== +import time from logbook import Logger from itertools import chain @@ -25,6 +26,7 @@ from sqlalchemy.orm import validates, reconstructor import eos import eos.db +import eos.config from eos.effectHandlerHelpers import HandledItem, HandledImplantBoosterList pyfalog = Logger(__name__) @@ -35,6 +37,36 @@ class Character(object): __itemIDMap = None __itemNameMap = None + def __init__(self, name, defaultLevel=None, initSkills=True): + self.savedName = name + self.__owner = None + self.defaultLevel = defaultLevel + self.__skills = [] + self.__skillIdMap = {} + self.dirtySkills = set() + self.alphaClone = None + self.__secStatus = 0.0 + + if initSkills: + for item in self.getSkillList(): + self.addSkill(Skill(item.ID, self.defaultLevel)) + + self.__implants = HandledImplantBoosterList() + self.apiKey = None + + @reconstructor + def init(self): + + self.__skillIdMap = {} + for skill in self.__skills: + self.__skillIdMap[skill.itemID] = skill + self.dirtySkills = set() + + self.alphaClone = None + + if self.alphaCloneID: + self.alphaClone = eos.db.getAlphaClone(self.alphaCloneID) + @classmethod def getSkillList(cls): if cls.__itemList is None: @@ -42,10 +74,6 @@ class Character(object): return cls.__itemList - @classmethod - def setSkillList(cls, list): - cls.__itemList = list - @classmethod def getSkillIDMap(cls): if cls.__itemIDMap is None: @@ -91,45 +119,29 @@ class Character(object): return all0 - def __init__(self, name, defaultLevel=None, initSkills=True): - self.savedName = name - self.__owner = None - self.defaultLevel = defaultLevel - self.__skills = [] - self.__skillIdMap = {} - self.dirtySkills = set() - self.alphaClone = None - - if initSkills: - for item in self.getSkillList(): - self.addSkill(Skill(item.ID, self.defaultLevel)) - - self.__implants = HandledImplantBoosterList() - self.apiKey = None - - @reconstructor - def init(self): - - self.__skillIdMap = {} - for skill in self.__skills: - self.__skillIdMap[skill.itemID] = skill - self.dirtySkills = set() - - self.alphaClone = None - - if self.alphaCloneID: - self.alphaClone = eos.db.getAlphaClone(self.alphaCloneID) - - def apiUpdateCharSheet(self, skills): + def apiUpdateCharSheet(self, skills, secStatus): del self.__skills[:] self.__skillIdMap.clear() for skillRow in skills: self.addSkill(Skill(skillRow["typeID"], skillRow["level"])) + self.secStatus = secStatus @property def ro(self): return self == self.getAll0() or self == self.getAll5() + @property + def secStatus(self): + if self.name == "All 5": + self.__secStatus = 5.00 + elif self.name == "All 0": + self.__secStatus = 0.00 + return self.__secStatus + + @secStatus.setter + def secStatus(self, sec): + self.__secStatus = sec + @property def owner(self): return self.__owner @@ -247,8 +259,8 @@ class Character(object): def clear(self): c = chain( - self.skills, - self.implants + self.skills, + self.implants ) for stuff in c: if stuff is not None and stuff != self: @@ -266,10 +278,12 @@ class Character(object): @validates("ID", "name", "apiKey", "ownerID") def validator(self, key, val): - map = {"ID": lambda _val: isinstance(_val, int), - "name": lambda _val: True, - "apiKey": lambda _val: _val is None or (isinstance(_val, basestring) and len(_val) > 0), - "ownerID": lambda _val: isinstance(_val, int) or _val is None} + map = { + "ID" : lambda _val: isinstance(_val, int), + "name" : lambda _val: True, + "apiKey" : lambda _val: _val is None or (isinstance(_val, basestring) and len(_val) > 0), + "ownerID": lambda _val: isinstance(_val, int) or _val is None + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) @@ -278,7 +292,7 @@ class Character(object): def __repr__(self): return "Character(ID={}, name={}) at {}".format( - self.ID, self.name, hex(id(self)) + self.ID, self.name, hex(id(self)) ) @@ -319,13 +333,18 @@ class Skill(HandledItem): @property def level(self): - if self.character.alphaClone: + # Ensure that All 5/0 character have proper skill levels (in case database gets corrupted) + if self.character.name == "All 5": + self.activeLevel = self.__level = 5 + elif self.character.name == "All 0": + self.activeLevel = self.__level = 0 + elif self.character.alphaClone: return min(self.activeLevel, self.character.alphaClone.getSkillLevel(self)) or 0 return self.activeLevel or 0 - @level.setter - def level(self, level): + def setLevel(self, level, persist=False): + if (level < 0 or level > 5) and level is not None: raise ValueError(str(level) + " is not a valid value for level") @@ -333,10 +352,24 @@ class Skill(HandledItem): raise ReadOnlyException() self.activeLevel = level - self.character.dirtySkills.add(self) - if self.activeLevel == self.__level and self in self.character.dirtySkills: - self.character.dirtySkills.remove(self) + if eos.config.settings['strictSkillLevels']: + start = time.time() + for item, rlevel in self.item.requiredFor.iteritems(): + if item.group.category.ID == 16: # Skill category + if level < rlevel: + skill = self.character.getSkill(item.ID) + # print "Removing skill: {}, Dependant level: {}, Required level: {}".format(skill, level, rlevel) + skill.setLevel(None, persist) + pyfalog.debug("Strict Skill levels enabled, time to process {}: {}".format(self.item.ID, time.time() - start)) + + if persist: + self.saveLevel() + else: + self.character.dirtySkills.add(self) + + if self.activeLevel == self.__level and self in self.character.dirtySkills: + self.character.dirtySkills.remove(self) @property def item(self): @@ -387,8 +420,10 @@ class Skill(HandledItem): if hasattr(self, "_Skill__ro") and self.__ro is True and key != "characterID": raise ReadOnlyException() - map = {"characterID": lambda _val: isinstance(_val, int), - "skillID": lambda _val: isinstance(_val, int)} + map = { + "characterID": lambda _val: isinstance(_val, int), + "skillID" : lambda _val: isinstance(_val, int) + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) @@ -401,7 +436,7 @@ class Skill(HandledItem): def __repr__(self): return "Skill(ID={}, name={}) at {}".format( - self.item.ID, self.item.name, hex(id(self)) + self.item.ID, self.item.name, hex(id(self)) ) diff --git a/eos/saveddata/citadel.py b/eos/saveddata/citadel.py index 90d7ab425..78bbde032 100644 --- a/eos/saveddata/citadel.py +++ b/eos/saveddata/citadel.py @@ -29,7 +29,7 @@ class Citadel(Ship): if item.category.name != "Structure": pyfalog.error("Passed item '{0}' (category: {1}) is not under Structure category", item.name, item.category.name) raise ValueError( - 'Passed item "%s" (category: (%s)) is not under Structure category' % (item.name, item.category.name)) + 'Passed item "%s" (category: (%s)) is not under Structure category' % (item.name, item.category.name)) def __deepcopy__(self, memo): copy = Citadel(self.item) @@ -37,5 +37,5 @@ class Citadel(Ship): def __repr__(self): return "Citadel(ID={}, name={}) at {}".format( - self.item.ID, self.item.name, hex(id(self)) + self.item.ID, self.item.name, hex(id(self)) ) diff --git a/eos/saveddata/crestchar.py b/eos/saveddata/crestchar.py index 51a32fe26..ce6ab14fe 100644 --- a/eos/saveddata/crestchar.py +++ b/eos/saveddata/crestchar.py @@ -32,13 +32,3 @@ class CrestChar(object): @reconstructor def init(self): pass - - ''' - @threads(1) - def fetchImage(self): - url = 'https://image.eveonline.com/character/%d_128.jpg'%self.ID - fp = urllib.urlopen(url) - data = fp.read() - fp.close() - self.img = StringIO(data) - ''' diff --git a/eos/saveddata/damagePattern.py b/eos/saveddata/damagePattern.py index f10e6b50b..2d32de6a2 100644 --- a/eos/saveddata/damagePattern.py +++ b/eos/saveddata/damagePattern.py @@ -62,10 +62,12 @@ class DamagePattern(object): return amount / (specificDivider or 1) - importMap = {"em": "em", - "therm": "thermal", - "kin": "kinetic", - "exp": "explosive"} + importMap = { + "em" : "em", + "therm": "thermal", + "kin" : "kinetic", + "exp" : "explosive" + } @classmethod def importPatterns(cls, text): diff --git a/eos/saveddata/drone.py b/eos/saveddata/drone.py index 82053290d..2ce1b52ab 100644 --- a/eos/saveddata/drone.py +++ b/eos/saveddata/drone.py @@ -138,8 +138,8 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): cycleTime = self.getModifiedItemAttr(attr) volley = sum( - map(lambda d: (getter("%sDamage" % d) or 0) * (1 - getattr(targetResists, "%sAmount" % d, 0)), - self.DAMAGE_TYPES)) + map(lambda d: (getter("%sDamage" % d) or 0) * (1 - getattr(targetResists, "%sAmount" % d, 0)), + self.DAMAGE_TYPES)) volley *= self.amountActive volley *= self.getModifiedItemAttr("damageMultiplier") or 1 self.__volley = volley @@ -190,11 +190,13 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @validates("ID", "itemID", "chargeID", "amount", "amountActive") def validator(self, key, val): - map = {"ID": lambda _val: isinstance(_val, int), - "itemID": lambda _val: isinstance(_val, int), - "chargeID": lambda _val: isinstance(_val, int), - "amount": lambda _val: isinstance(_val, int) and _val >= 0, - "amountActive": lambda _val: isinstance(_val, int) and self.amount >= _val >= 0} + map = { + "ID" : lambda _val: isinstance(_val, int), + "itemID" : lambda _val: isinstance(_val, int), + "chargeID" : lambda _val: isinstance(_val, int), + "amount" : lambda _val: isinstance(_val, int) and _val >= 0, + "amountActive": lambda _val: isinstance(_val, int) and self.amount >= _val >= 0 + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) @@ -236,9 +238,9 @@ class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): for effect in self.item.effects.itervalues(): if effect.runTime == runTime and \ - effect.activeByDefault and \ + effect.activeByDefault and \ ((projected is True and effect.isType("projected")) or - projected is False and effect.isType("passive")): + projected is False and effect.isType("passive")): # See GH issue #765 if effect.getattr('grouped'): effect.handler(fit, self, context) diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index e10b5b3c5..1ea07ebfd 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -101,9 +101,11 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): return [FighterAbility(effect) for effect in self.item.effects.values()] def __calculateSlot(self, item): - types = {"Light": Slot.F_LIGHT, - "Support": Slot.F_SUPPORT, - "Heavy": Slot.F_HEAVY} + types = { + "Light" : Slot.F_LIGHT, + "Support": Slot.F_SUPPORT, + "Heavy" : Slot.F_HEAVY + } for t, slot in types.iteritems(): if self.getModifiedItemAttr("fighterSquadronIs{}".format(t)): @@ -219,11 +221,12 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @validates("ID", "itemID", "chargeID", "amount", "amountActive") def validator(self, key, val): - map = {"ID": lambda _val: isinstance(_val, int), - "itemID": lambda _val: isinstance(_val, int), - "chargeID": lambda _val: isinstance(_val, int), - "amount": lambda _val: isinstance(_val, int) and _val >= -1, - } + map = { + "ID" : lambda _val: isinstance(_val, int), + "itemID" : lambda _val: isinstance(_val, int), + "chargeID": lambda _val: isinstance(_val, int), + "amount" : lambda _val: isinstance(_val, int) and _val >= -1, + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) diff --git a/eos/saveddata/fighterAbility.py b/eos/saveddata/fighterAbility.py index efe429c41..b6b099a5c 100644 --- a/eos/saveddata/fighterAbility.py +++ b/eos/saveddata/fighterAbility.py @@ -132,7 +132,7 @@ class FighterAbility(object): else: volley = sum(map(lambda d2, d: (self.fighter.getModifiedItemAttr( - "{}Damage{}".format(self.attrPrefix, d2)) or 0) * + "{}Damage{}".format(self.attrPrefix, d2)) or 0) * (1 - getattr(targetResists, "{}Amount".format(d), 0)), self.DAMAGE_TYPES2, self.DAMAGE_TYPES)) diff --git a/eos/saveddata/fit.py b/eos/saveddata/fit.py index c9dd3677c..b9e43c083 100644 --- a/eos/saveddata/fit.py +++ b/eos/saveddata/fit.py @@ -21,6 +21,7 @@ import time from copy import deepcopy from itertools import chain from math import sqrt, log, asinh +import datetime from sqlalchemy.orm import validates, reconstructor @@ -33,7 +34,6 @@ from eos.saveddata.drone import Drone from eos.saveddata.character import Character from eos.saveddata.citadel import Citadel from eos.saveddata.module import Module, State, Slot, Hardpoint -from utils.timer import Timer from logbook import Logger pyfalog = Logger(__name__) @@ -44,6 +44,12 @@ class ImplantLocation(Enum): CHARACTER = 1 +class CalcType(Enum): + LOCAL = 0 + PROJECTED = 1 + COMMAND = 2 + + class Fit(object): """Represents a fitting, with modules, ship, implants, etc.""" @@ -72,6 +78,8 @@ class Fit(object): self.projected = False self.name = name self.timestamp = time.time() + self.created = None + self.modified = None self.modeID = None self.build() @@ -175,6 +183,14 @@ class Fit(object): self.__mode = mode self.modeID = mode.item.ID if mode is not None else None + @property + def modifiedCoalesce(self): + """ + This is a property that should get whichever date is available for the fit. @todo: migrate old timestamp data + and ensure created / modified are set in database to get rid of this + """ + return self.modified or self.created or datetime.datetime.fromtimestamp(self.timestamp) + @property def character(self): return self.__character if self.__character is not None else Character.getAll0() @@ -183,6 +199,15 @@ class Fit(object): def character(self, char): self.__character = char + @property + def calculated(self): + return self.__calculated + + @calculated.setter + def calculated(self, bool): + # todo: brief explaination hwo this works + self.__calculated = bool + @property def ship(self): return self.__ship @@ -460,7 +485,7 @@ class Fit(object): self.commandBonuses[warfareBuffID] = (runTime, value, module, effect) def __runCommandBoosts(self, runTime="normal"): - pyfalog.debug("Applying gang boosts for {0}", self) + pyfalog.debug("Applying gang boosts for {0}", repr(self)) for warfareBuffID in self.commandBonuses.keys(): # Unpack all data required to run effect properly effect_runTime, value, thing, effect = self.commandBonuses[warfareBuffID] @@ -643,60 +668,84 @@ class Fit(object): del self.commandBonuses[warfareBuffID] - def calculateModifiedAttributes(self, targetFit=None, withBoosters=False, dirtyStorage=None): - timer = Timer(u'Fit: {}, {}'.format(self.ID, self.name), pyfalog) - pyfalog.debug("Starting fit calculation on: {0}, withBoosters: {1}", self, withBoosters) + def __resetDependentCalcs(self): + self.calculated = False + for value in self.projectedOnto.values(): + if value.victim_fit: # removing a self-projected fit causes victim fit to be None. @todo: look into why. :3 + value.victim_fit.calculated = False - shadow = False - if targetFit and not withBoosters: - pyfalog.debug("Applying projections to target: {0}", targetFit) + def calculateModifiedAttributes(self, targetFit=None, type=CalcType.LOCAL): + """ + The fit calculation function. It should be noted that this is a recursive function - if the local fit has + projected fits, this function will be called for those projected fits to be calculated. + + Args: + targetFit: + If this is set, signals that we are currently calculating a remote fit (projected or command) that + should apply it's remote effects to the targetFit. If None, signals that we are currently calcing the + local fit + type: + The type of calculation our current iteration is in. This helps us determine the interactions between + fits that rely on others for proper calculations + """ + pyfalog.debug("Starting fit calculation on: {0}, calc: {1}", repr(self), CalcType.getName(type)) + + # If we are projecting this fit onto another one, collect the projection info for later use + # We also deal with self-projection here by setting self as a copy (to get a new fit object) to apply onto original fit + # First and foremost, if we're looking at a local calc, reset the calculated state of fits that this fit affects + # Thankfully, due to the way projection mechanics currently work, we don't have to traverse down a projection + # tree to (resetting the first degree of projection will suffice) + if targetFit is None: + # This resets all fits that local projects onto, allowing them to recalc when loaded + self.__resetDependentCalcs() + + # For fits that are under local's Command, we do the same thing + for value in self.boostedOnto.values(): + # apparently this is a thing that happens when removing a command fit from a fit and then switching to + # that command fit. Same as projected clears, figure out why. + if value.boosted_fit: + value.boosted_fit.__resetDependentCalcs() + + if targetFit and type == CalcType.PROJECTED: + pyfalog.debug("Calculating projections from {0} to target {1}", repr(self), repr(targetFit)) projectionInfo = self.getProjectionInfo(targetFit.ID) - pyfalog.debug("ProjectionInfo: {0}", projectionInfo) - if self == targetFit: - copied = self # original fit - shadow = True - # Don't inspect this, we genuinely want to reassign self - # noinspection PyMethodFirstArgAssignment - self = deepcopy(self) - pyfalog.debug("Handling self projection - making shadow copy of fit. {0} => {1}", copied, self) - # we delete the fit because when we copy a fit, flush() is - # called to properly handle projection updates. However, we do - # not want to save this fit to the database, so simply remove it - eos.db.saveddata_session.delete(self) - if self.commandFits and not withBoosters: + # Start applying any command fits that we may have. + # We run the command calculations first so that they can calculate fully and store the command effects on the + # target fit to be used later on in the calculation. This does not apply when we're already calculating a + # command fit. + if type != CalcType.COMMAND and self.commandFits: for fit in self.commandFits: - if self == fit: + commandInfo = fit.getCommandInfo(self.ID) + # Continue loop if we're trying to apply ourselves or if this fit isn't active + if not commandInfo.active or self == commandInfo.booster_fit: continue - fit.calculateModifiedAttributes(self, True) + commandInfo.booster_fit.calculateModifiedAttributes(self, CalcType.COMMAND) # If we're not explicitly asked to project fit onto something, # set self as target fit if targetFit is None: targetFit = self - projected = False - else: - projected = not withBoosters # If fit is calculated and we have nothing to do here, get out - # A note on why projected fits don't get to return here. If we return - # here, the projection afflictions will not be run as they are - # intertwined into the regular fit calculations. So, even if the fit has - # been calculated, we need to recalculate it again just to apply the - # projections. This is in contract to gang boosts, which are only - # calculated once, and their items are then looped and accessed with - # self.gangBoosts.iteritems() - # We might be able to exit early in the fit calculations if we separate - # projections from the normal fit calculations. But we must ensure that - # projection have modifying stuff applied, such as gang boosts and other - # local modules that may help - if self.__calculated and not projected and not withBoosters: - pyfalog.debug("Fit has already been calculated and is not projected, returning: {0}", self) + # A note on why we only do this for local fits. There may be + # gains that we can do here after some evaluation, but right + # now we need the projected and command fits to continue in + # this function even if they are already calculated, since it + # is during those calculations that they apply their effect + # to the target fits. todo: We could probably skip local fit + # calculations if calculated, and instead to projections and + # command stuffs. ninja edit: this is probably already being + # done with the calculated conditional in the calc loop + if self.__calculated and type == CalcType.LOCAL: + pyfalog.debug("Fit has already been calculated and is local, returning: {0}", self) return + # Loop through our run times here. These determine which effects are run in which order. for runTime in ("early", "normal", "late"): + pyfalog.debug("Run time: {0}", runTime) # Items that are unrestricted. These items are run on the local fit # first and then projected onto the target fit it one is designated u = [ @@ -720,57 +769,63 @@ class Fit(object): # chain unrestricted and restricted into one iterable c = chain.from_iterable(u + r) - # We calculate gang bonuses first so that projected fits get them - # if self.gangBoosts is not None: - # self.__calculateGangBoosts(runTime) - for item in c: # Registering the item about to affect the fit allows us to # track "Affected By" relations correctly if item is not None: + # apply effects locally if this is first time running them on fit if not self.__calculated: - # apply effects locally if this is first time running them on fit self.register(item) item.calculateModifiedAttributes(self, runTime, False) - if targetFit and withBoosters and item in self.modules: + # Run command effects against target fit. We only have to worry about modules + if type == CalcType.COMMAND and item in self.modules: # Apply the gang boosts to target fit # targetFit.register(item, origin=self) item.calculateModifiedAttributes(targetFit, runTime, False, True) - if len(self.commandBonuses) > 0: - pyfalog.info("Command bonuses applied.") - pyfalog.debug(self.commandBonuses) + pyfalog.debug("Command Bonuses: {}".format(self.commandBonuses)) - if not withBoosters and self.commandBonuses: + # If we are calculating our local or projected fit and have command bonuses, apply them + if type != CalcType.COMMAND and self.commandBonuses: self.__runCommandBoosts(runTime) - # Projection effects have been broken out of the main loop, see GH issue #1081 - - if projected is True and projectionInfo: - for item in chain.from_iterable(u): - if item is not None: - # apply effects onto target fit - for _ in xrange(projectionInfo.amount): - targetFit.register(item, origin=self) - item.calculateModifiedAttributes(targetFit, runTime, True) - - timer.checkpoint('Done with runtime: %s' % runTime) + # Run projection effects against target fit. Projection effects have been broken out of the main loop, + # see GH issue #1081 + if type == CalcType.PROJECTED and projectionInfo: + self.__runProjectionEffects(runTime, targetFit, projectionInfo) # Mark fit as calculated self.__calculated = True # Only apply projected fits if fit it not projected itself. - if not projected and not withBoosters: + if type == CalcType.LOCAL: for fit in self.projectedFits: - if fit.getProjectionInfo(self.ID).active: - fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage) + projInfo = fit.getProjectionInfo(self.ID) + if projInfo.active: + if fit == self: + # If doing self projection, no need to run through the recursion process. Simply run the + # projection effects on ourselves + pyfalog.debug("Running self-projection for {0}", repr(self)) + for runTime in ("early", "normal", "late"): + self.__runProjectionEffects(runTime, self, projInfo) + else: + fit.calculateModifiedAttributes(self, type=CalcType.PROJECTED) - timer.checkpoint('Done with fit calculation') + pyfalog.debug('Done with fit calculation') - if shadow: - pyfalog.debug("Delete shadow fit object") - del self + def __runProjectionEffects(self, runTime, targetFit, projectionInfo): + """ + To support a simpler way of doing self projections (so that we don't have to make a copy of the fit and + recalculate), this function was developed to be a common source of projected effect application. + """ + c = chain(self.drones, self.fighters, self.modules) + for item in c: + if item is not None: + # apply effects onto target fit x amount of times + for _ in xrange(projectionInfo.amount): + targetFit.register(item, origin=self) + item.calculateModifiedAttributes(targetFit, runTime, True) def fill(self): """ @@ -1193,6 +1248,9 @@ class Fit(object): self.__remoteReps[remote_type] = 0 for stuff in chain(self.modules, self.drones): + if stuff.item: + if stuff.item.ID == 10250: + pass remote_type = None # Only apply the charged multiplier if we have a charge in our ancil reppers (#1135) @@ -1254,7 +1312,9 @@ class Fit(object): hp = droneHull else: hp = 0 - self.__remoteReps[remote_type] += (hp * modifier) / duration + + if hp > 0: + self.__remoteReps[remote_type] += (hp * modifier) / duration return self.__remoteReps @@ -1360,7 +1420,7 @@ class Fit(object): @property def fits(self): for mod in self.modules: - if not mod.fits(self): + if not mod.isEmpty and not mod.fits(self): return False return True diff --git a/eos/saveddata/implant.py b/eos/saveddata/implant.py index 240a51b0f..869e5c608 100644 --- a/eos/saveddata/implant.py +++ b/eos/saveddata/implant.py @@ -99,9 +99,11 @@ class Implant(HandledItem, ItemAttrShortcut): @validates("fitID", "itemID", "active") def validator(self, key, val): - map = {"fitID": lambda _val: isinstance(_val, int), - "itemID": lambda _val: isinstance(_val, int), - "active": lambda _val: isinstance(_val, bool)} + map = { + "fitID" : lambda _val: isinstance(_val, int), + "itemID": lambda _val: isinstance(_val, int), + "active": lambda _val: isinstance(_val, bool) + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) @@ -115,5 +117,5 @@ class Implant(HandledItem, ItemAttrShortcut): def __repr__(self): return "Implant(ID={}, name={}) at {}".format( - self.item.ID, self.item.name, hex(id(self)) + self.item.ID, self.item.name, hex(id(self)) ) diff --git a/eos/saveddata/mode.py b/eos/saveddata/mode.py index 2b3108a9d..50413906d 100644 --- a/eos/saveddata/mode.py +++ b/eos/saveddata/mode.py @@ -26,7 +26,7 @@ class Mode(ItemAttrShortcut, HandledItem): if item.group.name != "Ship Modifiers": raise ValueError( - 'Passed item "%s" (category: (%s)) is not a Ship Modifier' % (item.name, item.category.name)) + 'Passed item "%s" (category: (%s)) is not a Ship Modifier' % (item.name, item.category.name)) self.__item = item self.__itemModifiedAttributes = ModifiedAttributeDict() diff --git a/eos/saveddata/module.py b/eos/saveddata/module.py index 919b8c7b7..3834def33 100644 --- a/eos/saveddata/module.py +++ b/eos/saveddata/module.py @@ -330,8 +330,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): func = self.getModifiedItemAttr volley = sum(map( - lambda attr: (func("%sDamage" % attr) or 0) * (1 - getattr(targetResists, "%sAmount" % attr, 0)), - self.DAMAGE_TYPES)) + lambda attr: (func("%sDamage" % attr) or 0) * (1 - getattr(targetResists, "%sAmount" % attr, 0)), + self.DAMAGE_TYPES)) volley *= self.getModifiedItemAttr("damageMultiplier") or 1 if volley: cycleTime = self.cycleTime @@ -348,7 +348,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): else: if self.state >= State.ACTIVE: volley = self.getModifiedItemAttr("specialtyMiningAmount") or self.getModifiedItemAttr( - "miningAmount") or 0 + "miningAmount") or 0 if volley: cycleTime = self.cycleTime self.__miningyield = volley / (cycleTime / 1000.0) @@ -389,10 +389,24 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): self.__reloadForce = type def fits(self, fit, hardpointLimit=True): + """ + Function that determines if a module can be fit to the ship. We always apply slot restrictions no matter what + (too many assumptions made on this), however all other fitting restrictions are optional + """ + slot = self.slot if fit.getSlotsFree(slot) <= (0 if self.owner != fit else -1): return False + fits = self.__fitRestrictions(fit, hardpointLimit) + + if not fits and fit.ignoreRestrictions: + self.restrictionOverridden = True + fits = True + + return fits + + def __fitRestrictions(self, fit, hardpointLimit=True): # Check ship type restrictions fitsOnType = set() fitsOnGroup = set() @@ -413,8 +427,9 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if shipGroup is not None: fitsOnGroup.add(shipGroup) - if (len(fitsOnGroup) > 0 or len( - fitsOnType) > 0) and fit.ship.item.group.ID not in fitsOnGroup and fit.ship.item.ID not in fitsOnType: + if (len(fitsOnGroup) > 0 or len(fitsOnType) > 0) \ + and fit.ship.item.group.ID not in fitsOnGroup \ + and fit.ship.item.ID not in fitsOnType: return False # AFAIK Citadel modules will always be restricted based on canFitShipType/Group. If we are fitting to a Citadel @@ -424,7 +439,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): # EVE doesn't let capital modules be fit onto subcapital hulls. Confirmed by CCP Larrikin that this is dictated # by the modules volume. See GH issue #1096 - if (fit.ship.getModifiedItemAttr("isCapitalSize", 0) != 1 and self.isCapitalSize): + if not isinstance(fit.ship, Citadel) and fit.ship.getModifiedItemAttr("isCapitalSize", 0) != 1 and self.isCapitalSize: return False # If the mod is a subsystem, don't let two subs in the same slot fit @@ -559,8 +574,10 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @staticmethod def __calculateHardpoint(item): - effectHardpointMap = {"turretFitted": Hardpoint.TURRET, - "launcherFitted": Hardpoint.MISSILE} + effectHardpointMap = { + "turretFitted" : Hardpoint.TURRET, + "launcherFitted": Hardpoint.MISSILE + } if item is None: return Hardpoint.NONE @@ -573,12 +590,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @staticmethod def __calculateSlot(item): - effectSlotMap = {"rigSlot": Slot.RIG, - "loPower": Slot.LOW, - "medPower": Slot.MED, - "hiPower": Slot.HIGH, - "subSystem": Slot.SUBSYSTEM, - "serviceSlot": Slot.SERVICE} + effectSlotMap = { + "rigSlot" : Slot.RIG, + "loPower" : Slot.LOW, + "medPower" : Slot.MED, + "hiPower" : Slot.HIGH, + "subSystem" : Slot.SUBSYSTEM, + "serviceSlot": Slot.SERVICE + } if item is None: return None for effectName, slot in effectSlotMap.iteritems(): @@ -591,9 +610,11 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): @validates("ID", "itemID", "ammoID") def validator(self, key, val): - map = {"ID": lambda _val: isinstance(_val, int), - "itemID": lambda _val: _val is None or isinstance(_val, int), - "ammoID": lambda _val: isinstance(_val, int)} + map = { + "ID" : lambda _val: isinstance(_val, int), + "itemID": lambda _val: _val is None or isinstance(_val, int), + "ammoID": lambda _val: isinstance(_val, int) + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) @@ -637,8 +658,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): if effect.runTime == runTime and \ effect.activeByDefault and \ (effect.isType("offline") or - (effect.isType("passive") and self.state >= State.ONLINE) or - (effect.isType("active") and self.state >= State.ACTIVE)) and \ + (effect.isType("passive") and self.state >= State.ONLINE) or + (effect.isType("active") and self.state >= State.ACTIVE)) and \ (not gang or (gang and effect.isType("gang"))): chargeContext = ("moduleCharge",) @@ -759,8 +780,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def __repr__(self): if self.item: - return "Module(ID={}, name={}) at {}".format( - self.item.ID, self.item.name, hex(id(self)) + return u"Module(ID={}, name={}) at {}".format( + self.item.ID, self.item.name, hex(id(self)) ) else: return "EmptyModule() at {}".format(hex(id(self))) diff --git a/eos/saveddata/price.py b/eos/saveddata/price.py index 8074373df..304d6fedd 100644 --- a/eos/saveddata/price.py +++ b/eos/saveddata/price.py @@ -21,6 +21,9 @@ import time from sqlalchemy.orm import reconstructor +from logbook import Logger + +pyfalog = Logger(__name__) class Price(object): @@ -29,7 +32,6 @@ class Price(object): self.time = 0 self.price = 0 self.failed = None - self.__item = None @reconstructor def init(self): diff --git a/eos/saveddata/ship.py b/eos/saveddata/ship.py index 1e3fd41f0..90586dbd7 100644 --- a/eos/saveddata/ship.py +++ b/eos/saveddata/ship.py @@ -29,14 +29,14 @@ pyfalog = Logger(__name__) class Ship(ItemAttrShortcut, HandledItem): EXTRA_ATTRIBUTES = { - "armorRepair": 0, - "hullRepair": 0, - "shieldRepair": 0, - "maxActiveDrones": 0, + "armorRepair" : 0, + "hullRepair" : 0, + "shieldRepair" : 0, + "maxActiveDrones" : 0, "maxTargetsLockedFromSkills": 2, - "droneControlRange": 20000, - "cloaked": False, - "siege": False + "droneControlRange" : 20000, + "cloaked" : False, + "siege" : False # We also have speedLimit for Entosis Link, but there seems to be an # issue with naming it exactly "speedLimit" due to unknown reasons. # Regardless, we don't have to put it here anyways - it will come up @@ -65,12 +65,17 @@ class Ship(ItemAttrShortcut, HandledItem): if item.category.name != "Ship": pyfalog.error("Passed item '{0}' (category: {1}) is not under Ship category", item.name, item.category.name) raise ValueError( - 'Passed item "%s" (category: (%s)) is not under Ship category' % (item.name, item.category.name)) + 'Passed item "%s" (category: (%s)) is not under Ship category' % (item.name, item.category.name)) @property def item(self): return self.__item + @property + def name(self): + # NOTE: add name property + return self.__item.name + @property def itemModifiedAttributes(self): return self.__itemModifiedAttributes @@ -140,5 +145,5 @@ class Ship(ItemAttrShortcut, HandledItem): def __repr__(self): return "Ship(ID={}, name={}) at {}".format( - self.item.ID, self.item.name, hex(id(self)) + self.item.ID, self.item.name, hex(id(self)) ) diff --git a/eos/saveddata/user.py b/eos/saveddata/user.py index baf09d9f3..39e2eaed2 100644 --- a/eos/saveddata/user.py +++ b/eos/saveddata/user.py @@ -49,10 +49,12 @@ class User(object): @validates("ID", "username", "password", "admin") def validator(self, key, val): - map = {"ID": lambda _val: isinstance(_val, int), - "username": lambda _val: isinstance(_val, basestring), - "password": lambda _val: isinstance(_val, basestring) and len(_val) == 96, - "admin": lambda _val: isinstance(_val, bool)} + map = { + "ID" : lambda _val: isinstance(_val, int), + "username": lambda _val: isinstance(_val, basestring), + "password": lambda _val: isinstance(_val, basestring) and len(_val) == 96, + "admin" : lambda _val: isinstance(_val, bool) + } if not map[key](val): raise ValueError(str(val) + " is not a valid value for " + key) diff --git a/eve.db b/eve.db index f0717cc7c..a3a1f1d59 100644 Binary files a/eve.db and b/eve.db differ diff --git a/gui/boosterView.py b/gui/boosterView.py index fe0d802d3..db0e0eba5 100644 --- a/gui/boosterView.py +++ b/gui/boosterView.py @@ -43,9 +43,12 @@ class BoosterViewDrop(wx.PyDropTarget): class BoosterView(d.Display): - DEFAULT_COLS = ["State", - "attr:boosterness", - "Base Name"] + DEFAULT_COLS = [ + "State", + "attr:boosterness", + "Base Name", + "Price", + ] def __init__(self, parent): d.Display.__init__(self, parent, style=wx.LC_SINGLE_SEL | wx.BORDER_NONE) @@ -123,7 +126,7 @@ class BoosterView(d.Display): fit = sFit.getFit(fitID) - if fit.isStructure: + if not fit or fit.isStructure: return trigger = sFit.addBooster(fitID, event.itemID) diff --git a/gui/builtinContextMenus/changeAffectingSkills.py b/gui/builtinContextMenus/changeAffectingSkills.py index dc71fc06d..a708ec51f 100644 --- a/gui/builtinContextMenus/changeAffectingSkills.py +++ b/gui/builtinContextMenus/changeAffectingSkills.py @@ -20,7 +20,8 @@ class ChangeAffectingSkills(ContextMenu): if not self.settings.get('changeAffectingSkills'): return False - if self.mainFrame.getActiveFit() is None or srcContext not in ("fittingModule", "fittingCharge", "fittingShip"): + if self.mainFrame.getActiveFit() is None or srcContext not in ( + "fittingModule", "fittingCharge", "fittingShip", "droneItem", "fighterItem"): return False self.sChar = Character.getInstance() diff --git a/gui/builtinContextMenus/commandFits.py b/gui/builtinContextMenus/commandFits.py new file mode 100644 index 000000000..9640a83ef --- /dev/null +++ b/gui/builtinContextMenus/commandFits.py @@ -0,0 +1,96 @@ +# noinspection PyPackageRequirements +import wx + +from service.fit import Fit +from service.market import Market +import gui.mainFrame +import gui.globalEvents as GE +from gui.contextMenu import ContextMenu +from service.settings import ContextMenuSettings + + +class CommandFits(ContextMenu): + # Get list of items that define a command fit + sMkt = Market.getInstance() + grp = sMkt.getGroup(1770) # Command burst group + commandTypeIDs = [item.ID for item in grp.items] + commandFits = [] + menu = None + + @classmethod + def populateFits(cls, evt): + if evt is None or (getattr(evt, 'action', None) in ("modadd", "moddel") and getattr(evt, 'typeID', None) in cls.commandTypeIDs): + # we are adding or removing an item that defines a command fit. Need to refresh fit list + sFit = Fit.getInstance() + cls.commandFits = sFit.getFitsWithModules(cls.commandTypeIDs) + + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + self.settings = ContextMenuSettings.getInstance() + + def display(self, srcContext, selection): + if self.mainFrame.getActiveFit() is None or len(self.__class__.commandFits) == 0 or srcContext != "commandView": + return False + + return True + + def getText(self, itmContext, selection): + return "Command Fits" + + def addFit(self, menu, fit, includeShip=False): + label = fit.name if not includeShip else "({}) {}".format(fit.ship.item.name, fit.name) + id = ContextMenu.nextID() + self.fitMenuItemIds[id] = fit + menuItem = wx.MenuItem(menu, id, label) + menu.Bind(wx.EVT_MENU, self.handleSelection, menuItem) + return menuItem + + def getSubMenu(self, context, selection, rootMenu, i, pitem): + msw = True if "wxMSW" in wx.PlatformInfo else False + self.context = context + self.fitMenuItemIds = {} + + sub = wx.Menu() + + if len(self.__class__.commandFits) < 15: + for fit in sorted(self.__class__.commandFits, key=lambda x: x.name): + print fit + menuItem = self.addFit(rootMenu if msw else sub, fit, True) + sub.AppendItem(menuItem) + else: + typeDict = {} + + for fit in self.__class__.commandFits: + shipName = fit.ship.item.name + if shipName not in typeDict: + typeDict[shipName] = [] + typeDict[shipName].append(fit) + + for ship in sorted(typeDict.keys()): + shipItem = wx.MenuItem(sub, ContextMenu.nextID(), ship) + grandSub = wx.Menu() + shipItem.SetSubMenu(grandSub) + + for fit in sorted(typeDict[ship], key=lambda x: x.name): + fitItem = self.addFit(rootMenu if msw else grandSub, fit, False) + grandSub.AppendItem(fitItem) + + sub.AppendItem(shipItem) + + return sub + + def handleSelection(self, event): + fit = self.fitMenuItemIds[event.Id] + if fit is False or fit not in self.__class__.commandFits: + event.Skip() + return + + sFit = Fit.getInstance() + fitID = self.mainFrame.getActiveFit() + + sFit.addCommandFit(fitID, fit) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + + +CommandFits.populateFits(None) +CommandFits.register() diff --git a/gui/builtinContextMenus/droneSplit.py b/gui/builtinContextMenus/droneSplit.py index 761262a8c..7a121ae96 100644 --- a/gui/builtinContextMenus/droneSplit.py +++ b/gui/builtinContextMenus/droneSplit.py @@ -43,7 +43,7 @@ class DroneSpinner(wx.Dialog): self.spinner.SetRange(1, drone.amount - 1) self.spinner.SetValue(1) - bSizer1.Add(self.spinner, 0, wx.ALL, 5) + bSizer1.Add(self.spinner, 1, wx.ALL, 5) self.button = wx.Button(self, wx.ID_OK, u"Split") bSizer1.Add(self.button, 0, wx.ALL, 5) diff --git a/gui/builtinContextMenus/itemRemove.py b/gui/builtinContextMenus/itemRemove.py index 6ad128220..e5bb32d0e 100644 --- a/gui/builtinContextMenus/itemRemove.py +++ b/gui/builtinContextMenus/itemRemove.py @@ -21,12 +21,14 @@ class ItemRemove(ContextMenu): "boosterItem", "projectedModule", "projectedCharge", "cargoItem", "projectedFit", "projectedDrone", - "fighterItem", "projectedFighter") + "fighterItem", "projectedFighter", + "commandFit") def getText(self, itmContext, selection): return "Remove {0}".format(itmContext if itmContext is not None else "Item") def activate(self, fullContext, selection, i): + srcContext = fullContext[0] sFit = Fit.getInstance() fitID = self.mainFrame.getActiveFit() @@ -48,8 +50,10 @@ class ItemRemove(ContextMenu): sFit.removeBooster(fitID, fit.boosters.index(selection[0])) elif srcContext == "cargoItem": sFit.removeCargo(fitID, fit.cargo.index(selection[0])) - else: + elif srcContext == "projectedFit": sFit.removeProjected(fitID, selection[0]) + elif srcContext == "commandFit": + sFit.removeCommand(fitID, selection[0]) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) diff --git a/gui/builtinContextMenus/metaSwap.py b/gui/builtinContextMenus/metaSwap.py index 00766905f..950a9be8e 100644 --- a/gui/builtinContextMenus/metaSwap.py +++ b/gui/builtinContextMenus/metaSwap.py @@ -143,11 +143,11 @@ class MetaSwap(ContextMenu): for idx, drone_stack in enumerate(fit.drones): if drone_stack is selected_item: drone_count = drone_stack.amount - sFit.removeDrone(fitID, idx, drone_count) + sFit.removeDrone(fitID, idx, drone_count, False) break if drone_count: - sFit.addDrone(fitID, item.ID, drone_count) + sFit.addDrone(fitID, item.ID, drone_count, True) elif isinstance(selected_item, Fighter): fighter_count = None @@ -164,16 +164,16 @@ class MetaSwap(ContextMenu): else: fighter_count.amount = 0 - sFit.removeFighter(fitID, idx) + sFit.removeFighter(fitID, idx, False) break - sFit.addFighter(fitID, item.ID) + sFit.addFighter(fitID, item.ID, True) elif isinstance(selected_item, Booster): for idx, booster_stack in enumerate(fit.boosters): if booster_stack is selected_item: - sFit.removeBooster(fitID, idx) - sFit.addBooster(fitID, item.ID) + sFit.removeBooster(fitID, idx, False) + sFit.addBooster(fitID, item.ID, True) break elif isinstance(selected_item, Implant): diff --git a/gui/builtinContextMenus/openFit.py b/gui/builtinContextMenus/openFit.py index d13618132..2057e04c1 100644 --- a/gui/builtinContextMenus/openFit.py +++ b/gui/builtinContextMenus/openFit.py @@ -15,7 +15,7 @@ class OpenFit(ContextMenu): if not self.settings.get('openFit'): return False - return srcContext == "projectedFit" + return srcContext in ("projectedFit", "commandFit") def getText(self, itmContext, selection): return "Open Fit in New Tab" diff --git a/gui/builtinContextMenus/priceClear.py b/gui/builtinContextMenus/priceClear.py index 44093e662..9afdb0815 100644 --- a/gui/builtinContextMenus/priceClear.py +++ b/gui/builtinContextMenus/priceClear.py @@ -3,7 +3,7 @@ import gui.mainFrame # noinspection PyPackageRequirements import wx import gui.globalEvents as GE -from service.market import Market +from service.price import Price from service.settings import ContextMenuSettings @@ -16,14 +16,14 @@ class PriceClear(ContextMenu): if not self.settings.get('priceClear'): return False - return srcContext == "priceViewFull" + return srcContext in ("priceViewFull", "priceViewMinimal") def getText(self, itmContext, selection): return "Reset Price Cache" def activate(self, fullContext, selection, i): - sMkt = Market.getInstance() - sMkt.clearPriceCache() + sPrc = Price.getInstance() + sPrc.clearPriceCache() wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit())) diff --git a/gui/builtinContextMenus/tabbedFits.py b/gui/builtinContextMenus/tabbedFits.py new file mode 100644 index 000000000..c0db5973f --- /dev/null +++ b/gui/builtinContextMenus/tabbedFits.py @@ -0,0 +1,67 @@ +# coding: utf-8 + +# noinspection PyPackageRequirements +import wx + +from service.fit import Fit +import gui.mainFrame +import gui.globalEvents as GE +from gui.contextMenu import ContextMenu +from gui.builtinViews.emptyView import BlankPage + + +class TabbedFits(ContextMenu): + def __init__(self): + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + def display(self, srcContext, selection): + + if self.mainFrame.getActiveFit() is None or srcContext not in ("projected", "commandView"): + return False + + return True + + def getText(self, itmContext, selection): + return "Currently Open Fits" + + def getSubMenu(self, context, selection, rootMenu, i, pitem): + self.fitLookup = {} + self.context = context + sFit = Fit.getInstance() + + m = wx.Menu() + + # If on Windows we need to bind out events into the root menu, on other + # platforms they need to go to our sub menu + if "wxMSW" in wx.PlatformInfo: + bindmenu = rootMenu + else: + bindmenu = m + + for page in self.mainFrame.fitMultiSwitch.pages: + if isinstance(page, BlankPage): + continue + fit = sFit.getFit(page.activeFitID, basic=True) + id = ContextMenu.nextID() + mitem = wx.MenuItem(rootMenu, id, '{}: {}'.format(fit.ship.item.name, fit.name)) + bindmenu.Bind(wx.EVT_MENU, self.handleSelection, mitem) + self.fitLookup[id] = fit + m.AppendItem(mitem) + + return m + + def handleSelection(self, event): + sFit = Fit.getInstance() + fitID = self.mainFrame.getActiveFit() + + fit = self.fitLookup[event.Id] + + if self.context == 'commandView': + sFit.addCommandFit(fitID, fit) + elif self.context == 'projected': + sFit.project(fitID, fit) + + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + + +TabbedFits.register() diff --git a/gui/builtinPreferenceViews/pyfaDatabasePreferences.py b/gui/builtinPreferenceViews/pyfaDatabasePreferences.py index b8647ef2f..ab93d4655 100644 --- a/gui/builtinPreferenceViews/pyfaDatabasePreferences.py +++ b/gui/builtinPreferenceViews/pyfaDatabasePreferences.py @@ -2,7 +2,9 @@ import wx from gui.preferenceView import PreferenceView from gui.bitmapLoader import BitmapLoader +from gui.utils import helpers_wxPython as wxHelpers import config +from eos.db.saveddata.queries import clearPrices, clearDamagePatterns, clearTargetResists import logging @@ -14,7 +16,6 @@ class PFGeneralPref(PreferenceView): def populatePanel(self, panel): self.dirtySettings = False - # self.openFitsSettings = service.SettingsProvider.getInstance().getSettings("pyfaPrevOpenFits", {"enabled": False, "pyfaOpenFits": []}) mainSizer = wx.BoxSizer(wx.VERTICAL) @@ -67,13 +68,49 @@ class PFGeneralPref(PreferenceView): self.cbsaveInRoot.SetValue(config.saveInRoot) self.cbsaveInRoot.Bind(wx.EVT_CHECKBOX, self.onCBsaveInRoot) - self.inputUserPath.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave) - self.inputFitDB.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave) - self.inputGameDB.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave) + # self.inputUserPath.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave) + # self.inputFitDB.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave) + # self.inputGameDB.Bind(wx.EVT_LEAVE_WINDOW, self.OnWindowLeave) + + self.m_staticline3 = wx.StaticLine(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) + mainSizer.Add(self.m_staticline3, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 5) + + btnSizer = wx.BoxSizer(wx.VERTICAL) + btnSizer.AddSpacer((0, 0), 1, wx.EXPAND, 5) + + self.btnDeleteDamagePatterns = wx.Button(panel, wx.ID_ANY, u"Delete All Damage Pattern Profiles", wx.DefaultPosition, wx.DefaultSize, 0) + btnSizer.Add(self.btnDeleteDamagePatterns, 0, wx.ALL, 5) + + self.btnDeleteTargetResists = wx.Button(panel, wx.ID_ANY, u"Delete All Target Resist Profiles", wx.DefaultPosition, wx.DefaultSize, 0) + btnSizer.Add(self.btnDeleteTargetResists, 0, wx.ALL, 5) + + self.btnPrices = wx.Button(panel, wx.ID_ANY, u"Delete All Prices", wx.DefaultPosition, wx.DefaultSize, 0) + btnSizer.Add(self.btnPrices, 0, wx.ALL, 5) + + mainSizer.Add(btnSizer, 0, wx.EXPAND, 5) + + self.btnDeleteDamagePatterns.Bind(wx.EVT_BUTTON, self.DeleteDamagePatterns) + self.btnDeleteTargetResists.Bind(wx.EVT_BUTTON, self.DeleteTargetResists) + self.btnPrices.Bind(wx.EVT_BUTTON, self.DeletePrices) panel.SetSizer(mainSizer) panel.Layout() + def DeleteDamagePatterns(self, event): + question = u"This is a destructive action that will delete all damage pattern profiles.\nAre you sure you want to do this?" + if wxHelpers.YesNoDialog(question, "Confirm"): + clearDamagePatterns() + + def DeleteTargetResists(self, event): + question = u"This is a destructive action that will delete all target resist profiles.\nAre you sure you want to do this?" + if wxHelpers.YesNoDialog(question, "Confirm"): + clearTargetResists() + + def DeletePrices(self, event): + question = u"This is a destructive action that will delete all cached prices out of the database.\nAre you sure you want to do this?" + if wxHelpers.YesNoDialog(question, "Confirm"): + clearPrices() + def onCBsaveInRoot(self, event): # We don't want users to be able to actually change this, # so if they try and change it, set it back to the current setting @@ -87,26 +124,5 @@ class PFGeneralPref(PreferenceView): def getImage(self): return BitmapLoader.getBitmap("settings_database", "gui") - def OnWindowLeave(self, event): - # We don't want to do anything when they leave, - # but in the future we'd want to make sure settings - # changed get saved. - pass - - ''' - #Set database path - config.defPaths(self.inputFitDBPath.GetValue()) - - logger.debug("Running database import") - if self.cbimportDefaults is True: - # Import default database values - # Import values that must exist otherwise Pyfa breaks - DefaultDatabaseValues.importRequiredDefaults() - # Import default values for damage profiles - DefaultDatabaseValues.importDamageProfileDefaults() - # Import default values for target resist profiles - DefaultDatabaseValues.importResistProfileDefaults() - ''' - PFGeneralPref.register() diff --git a/gui/builtinPreferenceViews/pyfaEnginePreferences.py b/gui/builtinPreferenceViews/pyfaEnginePreferences.py index 7e7ebd72f..f9fba7612 100644 --- a/gui/builtinPreferenceViews/pyfaEnginePreferences.py +++ b/gui/builtinPreferenceViews/pyfaEnginePreferences.py @@ -5,6 +5,7 @@ import wx from service.fit import Fit from gui.bitmapLoader import BitmapLoader from gui.preferenceView import PreferenceView +from service.settings import EOSSettings logger = logging.getLogger(__name__) @@ -20,10 +21,13 @@ class PFFittingEnginePref(PreferenceView): # noinspection PyAttributeOutsideInit def populatePanel(self, panel): - # self.openFitsSettings = service.SettingsProvider.getInstance().getSettings("pyfaPrevOpenFits", {"enabled": False, "pyfaOpenFits": []}) mainSizer = wx.BoxSizer(wx.VERTICAL) + helpCursor = wx.StockCursor(wx.CURSOR_QUESTION_ARROW) + + self.engine_settings = EOSSettings.getInstance() + self.stTitle = wx.StaticText(panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0) self.stTitle.Wrap(-1) self.stTitle.SetFont(wx.Font(12, 70, 90, 90, False, wx.EmptyString)) @@ -34,8 +38,26 @@ class PFFittingEnginePref(PreferenceView): self.cbGlobalForceReload = wx.CheckBox(panel, wx.ID_ANY, u"Factor in reload time when calculating capacitor usage, damage, and tank.", wx.DefaultPosition, wx.DefaultSize, 0) + mainSizer.Add(self.cbGlobalForceReload, 0, wx.ALL | wx.EXPAND, 5) + self.cbStrictSkillLevels = wx.CheckBox(panel, wx.ID_ANY, + u"Enforce strict skill level requirements", + wx.DefaultPosition, wx.DefaultSize, 0) + self.cbStrictSkillLevels.SetCursor(helpCursor) + self.cbStrictSkillLevels.SetToolTip(wx.ToolTip( + u'When enabled, skills will check their dependencies\' requirements when their levels change and reset ' + + u'skills that no longer meet the requirement.\neg: Setting Drones from level V to IV will reset the Heavy ' + + u'Drone Operation skill, as that requires Drones V')) + + mainSizer.Add(self.cbStrictSkillLevels, 0, wx.ALL | wx.EXPAND, 5) + + self.cbUniversalAdaptiveArmorHardener = wx.CheckBox(panel, wx.ID_ANY, + u"When damage profile is Uniform, set Reactive Armor " + + u"Hardener to match (old behavior).", + wx.DefaultPosition, wx.DefaultSize, 0) + mainSizer.Add(self.cbUniversalAdaptiveArmorHardener, 0, wx.ALL | wx.EXPAND, 5) + # Future code once new cap sim is implemented ''' self.cbGlobalForceReactivationTimer = wx.CheckBox( panel, wx.ID_ANY, u"Factor in reactivation timer", wx.DefaultPosition, wx.DefaultSize, 0 ) @@ -63,15 +85,26 @@ class PFFittingEnginePref(PreferenceView): self.sFit = Fit.getInstance() self.cbGlobalForceReload.SetValue(self.sFit.serviceFittingOptions["useGlobalForceReload"]) - self.cbGlobalForceReload.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalForceReloadStateChange) + self.cbStrictSkillLevels.SetValue(self.engine_settings.get("strictSkillLevels")) + self.cbStrictSkillLevels.Bind(wx.EVT_CHECKBOX, self.OnCBStrictSkillLevelsChange) + + self.cbUniversalAdaptiveArmorHardener.SetValue(self.engine_settings.get("useStaticAdaptiveArmorHardener")) + self.cbUniversalAdaptiveArmorHardener.Bind(wx.EVT_CHECKBOX, self.OnCBUniversalAdaptiveArmorHardenerChange) + panel.SetSizer(mainSizer) panel.Layout() def OnCBGlobalForceReloadStateChange(self, event): self.sFit.serviceFittingOptions["useGlobalForceReload"] = self.cbGlobalForceReload.GetValue() + def OnCBStrictSkillLevelsChange(self, event): + self.engine_settings.set("strictSkillLevels", self.cbStrictSkillLevels.GetValue()) + + def OnCBUniversalAdaptiveArmorHardenerChange(self, event): + self.engine_settings.set("useStaticAdaptiveArmorHardener", self.cbUniversalAdaptiveArmorHardener.GetValue()) + def getImage(self): return BitmapLoader.getBitmap("settings_fitting", "gui") diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index 8a278a048..acdfb398b 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -1,5 +1,6 @@ # noinspection PyPackageRequirements import wx +from wx.lib.intctrl import IntCtrl from gui.preferenceView import PreferenceView from gui.bitmapLoader import BitmapLoader @@ -9,7 +10,6 @@ import gui.globalEvents as GE from service.settings import SettingsProvider from service.fit import Fit from service.price import Price -from service.market import Market class PFGeneralPref(PreferenceView): @@ -21,6 +21,8 @@ class PFGeneralPref(PreferenceView): self.openFitsSettings = SettingsProvider.getInstance().getSettings("pyfaPrevOpenFits", {"enabled": False, "pyfaOpenFits": []}) + helpCursor = wx.StockCursor(wx.CURSOR_QUESTION_ARROW) + mainSizer = wx.BoxSizer(wx.VERTICAL) self.stTitle = wx.StaticText(panel, wx.ID_ANY, self.title, wx.DefaultPosition, wx.DefaultSize, 0) @@ -78,6 +80,10 @@ class PFGeneralPref(PreferenceView): wx.DefaultPosition, wx.DefaultSize, 0) mainSizer.Add(self.cbOpenFitInNew, 0, wx.ALL | wx.EXPAND, 5) + self.cbShowShipBrowserTooltip = wx.CheckBox(panel, wx.ID_ANY, u"Show ship browser tooltip", + wx.DefaultPosition, wx.DefaultSize, 0) + mainSizer.Add(self.cbShowShipBrowserTooltip, 0, wx.ALL | wx.EXPAND, 5) + priceSizer = wx.BoxSizer(wx.HORIZONTAL) self.stDefaultSystem = wx.StaticText(panel, wx.ID_ANY, u"Default Market Prices:", wx.DefaultPosition, wx.DefaultSize, 0) @@ -89,6 +95,21 @@ class PFGeneralPref(PreferenceView): mainSizer.Add(priceSizer, 0, wx.ALL | wx.EXPAND, 0) + delayTimer = wx.BoxSizer(wx.HORIZONTAL) + + self.stMarketDelay = wx.StaticText(panel, wx.ID_ANY, u"Market Search Delay (ms):", wx.DefaultPosition, wx.DefaultSize, 0) + self.stMarketDelay.Wrap(-1) + self.stMarketDelay.SetCursor(helpCursor) + self.stMarketDelay.SetToolTip( + wx.ToolTip('The delay between a keystroke and the market search. Can help reduce lag when typing fast in the market search box.')) + + delayTimer.Add(self.stMarketDelay, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + self.intDelay = IntCtrl(panel, max=1000, limited=True) + delayTimer.Add(self.intDelay, 0, wx.ALL, 5) + + mainSizer.Add(delayTimer, 0, wx.ALL | wx.EXPAND, 0) + self.sFit = Fit.getInstance() self.cbGlobalChar.SetValue(self.sFit.serviceFittingOptions["useGlobalCharacter"]) @@ -104,6 +125,8 @@ class PFGeneralPref(PreferenceView): self.cbExportCharges.SetValue(self.sFit.serviceFittingOptions["exportCharges"]) self.cbOpenFitInNew.SetValue(self.sFit.serviceFittingOptions["openFitInNew"]) self.chPriceSystem.SetStringSelection(self.sFit.serviceFittingOptions["priceSystem"]) + self.cbShowShipBrowserTooltip.SetValue(self.sFit.serviceFittingOptions["showShipBrowserTooltip"]) + self.intDelay.SetValue(self.sFit.serviceFittingOptions["marketSearchDelay"]) self.cbGlobalChar.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalCharStateChange) self.cbGlobalDmgPattern.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalDmgPatternStateChange) @@ -118,12 +141,18 @@ class PFGeneralPref(PreferenceView): self.cbExportCharges.Bind(wx.EVT_CHECKBOX, self.onCBExportCharges) self.cbOpenFitInNew.Bind(wx.EVT_CHECKBOX, self.onCBOpenFitInNew) self.chPriceSystem.Bind(wx.EVT_CHOICE, self.onPriceSelection) + self.cbShowShipBrowserTooltip.Bind(wx.EVT_CHECKBOX, self.onCBShowShipBrowserTooltip) + self.intDelay.Bind(wx.lib.intctrl.EVT_INT, self.onMarketDelayChange) self.cbRackLabels.Enable(self.sFit.serviceFittingOptions["rackSlots"] or False) panel.SetSizer(mainSizer) panel.Layout() + def onMarketDelayChange(self, event): + self.sFit.serviceFittingOptions["marketSearchDelay"] = self.intDelay.GetValue() + event.Skip() + def onCBGlobalColorBySlot(self, event): self.sFit.serviceFittingOptions["colorFitBySlot"] = self.cbFitColorSlots.GetValue() fitID = self.mainFrame.getActiveFit() @@ -179,6 +208,9 @@ class PFGeneralPref(PreferenceView): def onCBOpenFitInNew(self, event): self.sFit.serviceFittingOptions["openFitInNew"] = self.cbOpenFitInNew.GetValue() + def onCBShowShipBrowserTooltip(self, event): + self.sFit.serviceFittingOptions["showShipBrowserTooltip"] = self.cbShowShipBrowserTooltip.GetValue() + def getImage(self): return BitmapLoader.getBitmap("prefs_settings", "gui") @@ -188,9 +220,6 @@ class PFGeneralPref(PreferenceView): fitID = self.mainFrame.getActiveFit() - sMkt = Market.getInstance() - sMkt.clearPriceCache() - self.sFit.refreshFit(fitID) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) event.Skip() diff --git a/gui/builtinPreferenceViews/pyfaHTMLExportPreferences.py b/gui/builtinPreferenceViews/pyfaHTMLExportPreferences.py index 01f00409d..a60cc24df 100644 --- a/gui/builtinPreferenceViews/pyfaHTMLExportPreferences.py +++ b/gui/builtinPreferenceViews/pyfaHTMLExportPreferences.py @@ -14,12 +14,9 @@ class PFHTMLExportPref(PreferenceView): title = "HTML Export" desc = ("HTML Export (File > Export HTML) allows you to export your entire fitting " "database into an HTML file at the specified location. This file can be " - "used in the in-game browser to easily open and import your fits, or used " - "in a regular web browser to open them at NULL-SEC.com or Osmium.") - desc2 = ("Enabling automatic exporting will update the HTML file after any change " - "to a fit is made. Under certain circumstance, this may cause performance issues.") - desc4 = ("Export Fittings in a minmal HTML Version, just containing the Fittingslinks " - "without any visual styling or javscript features") + "used to easily open your fits in a web-based fitting program") + desc4 = ("Export Fittings in a minimal HTML Version, just containing the fittings links " + "without any visual styling") def populatePanel(self, panel): self.mainFrame = gui.mainFrame.MainFrame.getInstance() @@ -55,21 +52,11 @@ class PFHTMLExportPref(PreferenceView): self.fileSelectButton.Bind(wx.EVT_BUTTON, self.selectHTMLExportFilePath) mainSizer.Add(self.fileSelectButton, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) - self.stDesc2 = wx.StaticText(panel, wx.ID_ANY, self.desc2, wx.DefaultPosition, wx.DefaultSize, 0) - self.stDesc2.Wrap(dlgWidth - 50) - mainSizer.Add(self.stDesc2, 0, wx.ALL, 5) - - self.exportEnabled = wx.CheckBox(panel, wx.ID_ANY, u"Enable automatic HTML export", wx.DefaultPosition, - wx.DefaultSize, 0) - self.exportEnabled.SetValue(self.HTMLExportSettings.getEnabled()) - self.exportEnabled.Bind(wx.EVT_CHECKBOX, self.OnExportEnabledChange) - mainSizer.Add(self.exportEnabled, 0, wx.ALL | wx.EXPAND, 5) - self.stDesc4 = wx.StaticText(panel, wx.ID_ANY, self.desc4, wx.DefaultPosition, wx.DefaultSize, 0) self.stDesc4.Wrap(dlgWidth - 50) mainSizer.Add(self.stDesc4, 0, wx.ALL, 5) - self.exportMinimal = wx.CheckBox(panel, wx.ID_ANY, u"Enable minimal export Format", wx.DefaultPosition, + self.exportMinimal = wx.CheckBox(panel, wx.ID_ANY, u"Enable minimal format", wx.DefaultPosition, wx.DefaultSize, 0) self.exportMinimal.SetValue(self.HTMLExportSettings.getMinimalEnabled()) self.exportMinimal.Bind(wx.EVT_CHECKBOX, self.OnMinimalEnabledChange) @@ -90,9 +77,6 @@ class PFHTMLExportPref(PreferenceView): self.dirtySettings = True self.setPathLinkCtrlValues(self.HTMLExportSettings.getPath()) - def OnExportEnabledChange(self, event): - self.HTMLExportSettings.setEnabled(self.exportEnabled.GetValue()) - def OnMinimalEnabledChange(self, event): self.HTMLExportSettings.setMinimalEnabled(self.exportMinimal.GetValue()) diff --git a/gui/builtinPreferenceViews/pyfaLoggingPreferences.py b/gui/builtinPreferenceViews/pyfaLoggingPreferences.py index 82f3b0b83..5a7ffccc1 100644 --- a/gui/builtinPreferenceViews/pyfaLoggingPreferences.py +++ b/gui/builtinPreferenceViews/pyfaLoggingPreferences.py @@ -3,6 +3,13 @@ import wx from gui.preferenceView import PreferenceView from gui.bitmapLoader import BitmapLoader import config +from logbook import Logger + +pyfalog = Logger(__name__) + + +def OnDumpLogs(event): + pyfalog.critical("Dump log button was pressed. Writing all logs to log file.") class PFGeneralPref(PreferenceView): @@ -10,7 +17,6 @@ class PFGeneralPref(PreferenceView): def populatePanel(self, panel): self.dirtySettings = False - # self.openFitsSettings = service.SettingsProvider.getInstance().getSettings("pyfaPrevOpenFits", {"enabled": False, "pyfaOpenFits": []}) mainSizer = wx.BoxSizer(wx.VERTICAL) @@ -27,10 +33,26 @@ 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) + # Database path + self.stLogPath = wx.StaticText(panel, wx.ID_ANY, u"Log file location:", wx.DefaultPosition, wx.DefaultSize, 0) + self.stLogPath.Wrap(-1) + mainSizer.Add(self.stLogPath, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + self.inputLogPath = wx.TextCtrl(panel, wx.ID_ANY, config.logPath, wx.DefaultPosition, wx.DefaultSize, 0) + self.inputLogPath.SetEditable(False) + self.inputLogPath.SetBackgroundColour((200, 200, 200)) + mainSizer.Add(self.inputLogPath, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5) + # Debug Logging self.cbdebugLogging = wx.CheckBox(panel, wx.ID_ANY, u"Debug Logging Enabled", wx.DefaultPosition, wx.DefaultSize, 0) mainSizer.Add(self.cbdebugLogging, 0, wx.ALL | wx.EXPAND, 5) + self.stDumpLogs = wx.StaticText(panel, wx.ID_ANY, u"Pressing this button will cause all logs in memory to write to the log file:", + wx.DefaultPosition, wx.DefaultSize, 0) + mainSizer.Add(self.stDumpLogs, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + self.btnDumpLogs = wx.Button(panel, wx.ID_ANY, u"Dump All Logs", wx.DefaultPosition, wx.DefaultSize, 0) + self.btnDumpLogs.Bind(wx.EVT_BUTTON, OnDumpLogs) + mainSizer.Add(self.btnDumpLogs, 0, wx.ALIGN_LEFT, 5) + self.cbdebugLogging.SetValue(config.debug) self.cbdebugLogging.Bind(wx.EVT_CHECKBOX, self.onCBdebugLogging) diff --git a/gui/builtinPreferenceViews/pyfaStatViewPreferences.py b/gui/builtinPreferenceViews/pyfaStatViewPreferences.py index 64182a82e..800b64851 100644 --- a/gui/builtinPreferenceViews/pyfaStatViewPreferences.py +++ b/gui/builtinPreferenceViews/pyfaStatViewPreferences.py @@ -91,8 +91,6 @@ class PFStatViewPref(PreferenceView): rbSizerRow3 = wx.BoxSizer(wx.HORIZONTAL) self.rbPrice = wx.RadioBox(panel, -1, "Price", wx.DefaultPosition, wx.DefaultSize, ['None', 'Minimal', 'Full'], 1, wx.RA_SPECIFY_COLS) - # Disable minimal as we don't have a view for this yet - self.rbPrice.EnableItem(1, False) self.rbPrice.SetSelection(self.settings.get('price')) rbSizerRow3.Add(self.rbPrice, 1, wx.TOP | wx.RIGHT, 5) self.rbPrice.Bind(wx.EVT_RADIOBOX, self.OnPriceChange) diff --git a/gui/builtinStatsViews/__init__.py b/gui/builtinStatsViews/__init__.py index be310c2ac..8769c7aef 100644 --- a/gui/builtinStatsViews/__init__.py +++ b/gui/builtinStatsViews/__init__.py @@ -8,4 +8,5 @@ __all__ = [ "outgoingViewMinimal", "targetingMiscViewMinimal", "priceViewFull", + "priceViewMinimal", ] diff --git a/gui/builtinStatsViews/capacitorViewFull.py b/gui/builtinStatsViews/capacitorViewFull.py index f85b35e4c..49bb9d59b 100644 --- a/gui/builtinStatsViews/capacitorViewFull.py +++ b/gui/builtinStatsViews/capacitorViewFull.py @@ -114,6 +114,10 @@ class CapacitorViewFull(StatsView): ("label%sCapacitorRecharge", lambda: fit.capRecharge, 3, 0, 0), ("label%sCapacitorDischarge", lambda: fit.capUsed, 3, 0, 0), ) + if fit: + neut_resist = fit.ship.getModifiedItemAttr("energyWarfareResistance", 0) + else: + neut_resist = 0 panel = "Full" for labelName, value, prec, lowest, highest in stats: @@ -127,6 +131,12 @@ class CapacitorViewFull(StatsView): label.SetLabel(formatAmount(value, prec, lowest, highest)) label.SetToolTip(wx.ToolTip("%.1f" % value)) + if labelName == "label%sCapacitorDischarge": + if neut_resist: + neut_resist = 100 - (neut_resist * 100) + label_tooltip = "Neut Resistance: {0:.0f}%".format(neut_resist) + label.SetToolTip(wx.ToolTip(label_tooltip)) + capState = fit.capState if fit is not None else 0 capStable = fit.capStable if fit is not None else False lblNameTime = "label%sCapacitorTime" diff --git a/gui/builtinStatsViews/priceViewFull.py b/gui/builtinStatsViews/priceViewFull.py index 0fbd24ff0..0b7ea8df6 100644 --- a/gui/builtinStatsViews/priceViewFull.py +++ b/gui/builtinStatsViews/priceViewFull.py @@ -22,7 +22,6 @@ import wx from gui.statsView import StatsView from gui.bitmapLoader import BitmapLoader from gui.utils.numberFormatter import formatAmount -from service.market import Market from service.price import Price @@ -32,9 +31,6 @@ class PriceViewFull(StatsView): def __init__(self, parent): StatsView.__init__(self) self.parent = parent - self._cachedShip = 0 - self._cachedFittings = 0 - self._cachedTotal = 0 def getHeaderText(self, fit): return "Price" @@ -51,10 +47,16 @@ class PriceViewFull(StatsView): headerContentSizer.Add(self.labelEMStatus) headerPanel.GetParent().AddToggleItem(self.labelEMStatus) - gridPrice = wx.GridSizer(1, 3) + gridPrice = wx.GridSizer(2, 3) contentSizer.Add(gridPrice, 0, wx.EXPAND | wx.ALL, 0) - for type in ("ship", "fittings", "total"): - image = "%sPrice_big" % type if type != "ship" else "ship_big" + for _type in ("ship", "fittings", "drones", "cargoBay", "character", "total"): + if _type in "ship": + image = "ship_big" + elif _type in ("fittings", "total"): + image = "%sPrice_big" % _type + else: + image = "%s_big" % _type + box = wx.BoxSizer(wx.HORIZONTAL) gridPrice.Add(box, 0, wx.ALIGN_TOP) @@ -63,50 +65,91 @@ class PriceViewFull(StatsView): vbox = wx.BoxSizer(wx.VERTICAL) box.Add(vbox, 1, wx.EXPAND) - vbox.Add(wx.StaticText(contentPanel, wx.ID_ANY, type.capitalize()), 0, wx.ALIGN_LEFT) + vbox.Add(wx.StaticText(contentPanel, wx.ID_ANY, _type.capitalize()), 0, wx.ALIGN_LEFT) hbox = wx.BoxSizer(wx.HORIZONTAL) vbox.Add(hbox) lbl = wx.StaticText(contentPanel, wx.ID_ANY, "0.00 ISK") - setattr(self, "labelPrice%s" % type.capitalize(), lbl) + setattr(self, "labelPrice%s" % _type.capitalize(), lbl) hbox.Add(lbl, 0, wx.ALIGN_LEFT) def refreshPanel(self, fit): if fit is not None: self.fit = fit - typeIDs = Price.fitItemsList(fit) + fit_items = Price.fitItemsList(fit) - sMkt = Market.getInstance() - sMkt.getPrices(typeIDs, self.processPrices) + sPrice = Price.getInstance() + sPrice.getPrices(fit_items, self.processPrices) self.labelEMStatus.SetLabel("Updating prices...") - else: - self.labelEMStatus.SetLabel("") - self.labelPriceShip.SetLabel("0.0 ISK") - self.labelPriceFittings.SetLabel("0.0 ISK") - self.labelPriceTotal.SetLabel("0.0 ISK") - self._cachedFittings = self._cachedShip = self._cachedTotal = 0 - self.panel.Layout() + + self.refreshPanelPrices(fit) + self.panel.Layout() + + def refreshPanelPrices(self, fit=None): + + ship_price = 0 + module_price = 0 + drone_price = 0 + fighter_price = 0 + cargo_price = 0 + booster_price = 0 + implant_price = 0 + + if fit: + ship_price = fit.ship.item.price.price + + if fit.modules: + for module in fit.modules: + if not module.isEmpty: + module_price += module.item.price.price + + if fit.drones: + for drone in fit.drones: + drone_price += drone.item.price.price * drone.amount + + if fit.fighters: + for fighter in fit.fighters: + fighter_price += fighter.item.price.price * fighter.amount + + if fit.cargo: + for cargo in fit.cargo: + cargo_price += cargo.item.price.price * cargo.amount + + if fit.boosters: + for booster in fit.boosters: + booster_price += booster.item.price.price + + if fit.implants: + for implant in fit.implants: + implant_price += implant.item.price.price + + fitting_price = module_price + drone_price + fighter_price + cargo_price + booster_price + implant_price + total_price = ship_price + fitting_price + + self.labelPriceShip.SetLabel("%s ISK" % formatAmount(ship_price, 3, 3, 9, currency=True)) + self.labelPriceShip.SetToolTip(wx.ToolTip('{:,.2f}'.format(ship_price))) + + self.labelPriceFittings.SetLabel("%s ISK" % formatAmount(module_price, 3, 3, 9, currency=True)) + self.labelPriceFittings.SetToolTip(wx.ToolTip('{:,.2f}'.format(module_price))) + + self.labelPriceDrones.SetLabel("%s ISK" % formatAmount(drone_price + fighter_price, 3, 3, 9, currency=True)) + self.labelPriceDrones.SetToolTip(wx.ToolTip('{:,.2f}'.format(drone_price + fighter_price))) + + self.labelPriceCargobay.SetLabel("%s ISK" % formatAmount(cargo_price, 3, 3, 9, currency=True)) + self.labelPriceCargobay.SetToolTip(wx.ToolTip('{:,.2f}'.format(cargo_price))) + + self.labelPriceCharacter.SetLabel("%s ISK" % formatAmount(booster_price + implant_price, 3, 3, 9, currency=True)) + self.labelPriceCharacter.SetToolTip(wx.ToolTip('{:,.2f}'.format(booster_price + implant_price))) + + self.labelPriceTotal.SetLabel("%s ISK" % formatAmount(total_price, 3, 3, 9, currency=True)) + self.labelPriceTotal.SetToolTip(wx.ToolTip('{:,.2f}'.format(total_price))) def processPrices(self, prices): - shipPrice = prices[0].price - modPrice = sum(map(lambda p: p.price or 0, prices[1:])) + self.refreshPanelPrices(self.fit) self.labelEMStatus.SetLabel("") - - if self._cachedShip != shipPrice: - self.labelPriceShip.SetLabel("%s ISK" % formatAmount(shipPrice, 3, 3, 9, currency=True)) - self.labelPriceShip.SetToolTip(wx.ToolTip('{:,.2f}'.format(shipPrice))) - self._cachedShip = shipPrice - if self._cachedFittings != modPrice: - self.labelPriceFittings.SetLabel("%s ISK" % formatAmount(modPrice, 3, 3, 9, currency=True)) - self.labelPriceFittings.SetToolTip(wx.ToolTip('{:,.2f}'.format(modPrice))) - self._cachedFittings = modPrice - if self._cachedTotal != (shipPrice + modPrice): - self.labelPriceTotal.SetLabel("%s ISK" % formatAmount(shipPrice + modPrice, 3, 3, 9, currency=True)) - self.labelPriceTotal.SetToolTip(wx.ToolTip('{:,.2f}'.format(shipPrice + modPrice))) - self._cachedTotal = shipPrice + modPrice self.panel.Layout() diff --git a/gui/builtinStatsViews/priceViewMinimal.py b/gui/builtinStatsViews/priceViewMinimal.py new file mode 100644 index 000000000..421d3d983 --- /dev/null +++ b/gui/builtinStatsViews/priceViewMinimal.py @@ -0,0 +1,141 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# This file is part of pyfa. +# +# pyfa is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pyfa is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pyfa. If not, see . +# ============================================================================= + +# noinspection PyPackageRequirements +import wx +from gui.statsView import StatsView +from gui.bitmapLoader import BitmapLoader +from gui.utils.numberFormatter import formatAmount +from service.price import Price + + +class PriceViewMinimal(StatsView): + name = "priceViewMinimal" + + def __init__(self, parent): + StatsView.__init__(self) + self.parent = parent + + def getHeaderText(self, fit): + return "Price" + + def populatePanel(self, contentPanel, headerPanel): + contentSizer = contentPanel.GetSizer() + self.panel = contentPanel + self.headerPanel = headerPanel + + headerContentSizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer = headerPanel.GetSizer() + hsizer.Add(headerContentSizer, 0, 0, 0) + self.labelEMStatus = wx.StaticText(headerPanel, wx.ID_ANY, "") + headerContentSizer.Add(self.labelEMStatus) + headerPanel.GetParent().AddToggleItem(self.labelEMStatus) + + gridPrice = wx.GridSizer(1, 3) + contentSizer.Add(gridPrice, 0, wx.EXPAND | wx.ALL, 0) + for _type in ("ship", "fittings", "total"): + image = "%sPrice_big" % _type if _type != "ship" else "ship_big" + box = wx.BoxSizer(wx.HORIZONTAL) + gridPrice.Add(box, 0, wx.ALIGN_TOP) + + box.Add(BitmapLoader.getStaticBitmap(image, contentPanel, "gui"), 0, wx.ALIGN_CENTER) + + vbox = wx.BoxSizer(wx.VERTICAL) + box.Add(vbox, 1, wx.EXPAND) + + vbox.Add(wx.StaticText(contentPanel, wx.ID_ANY, _type.capitalize()), 0, wx.ALIGN_LEFT) + + hbox = wx.BoxSizer(wx.HORIZONTAL) + vbox.Add(hbox) + + lbl = wx.StaticText(contentPanel, wx.ID_ANY, "0.00 ISK") + setattr(self, "labelPrice%s" % _type.capitalize(), lbl) + hbox.Add(lbl, 0, wx.ALIGN_LEFT) + + def refreshPanel(self, fit): + if fit is not None: + self.fit = fit + + fit_items = Price.fitItemsList(fit) + + sPrice = Price.getInstance() + sPrice.getPrices(fit_items, self.processPrices) + self.labelEMStatus.SetLabel("Updating prices...") + + self.refreshPanelPrices(fit) + self.panel.Layout() + + def refreshPanelPrices(self, fit=None): + + ship_price = 0 + module_price = 0 + drone_price = 0 + fighter_price = 0 + cargo_price = 0 + booster_price = 0 + implant_price = 0 + + if fit: + ship_price = fit.ship.item.price.price + + if fit.modules: + for module in fit.modules: + if not module.isEmpty: + module_price += module.item.price.price + + if fit.drones: + for drone in fit.drones: + drone_price += drone.item.price.price * drone.amount + + if fit.fighters: + for fighter in fit.fighters: + fighter_price += fighter.item.price.price * fighter.amount + + if fit.cargo: + for cargo in fit.cargo: + cargo_price += cargo.item.price.price * cargo.amount + + if fit.boosters: + for booster in fit.boosters: + booster_price += booster.item.price.price + + if fit.implants: + for implant in fit.implants: + implant_price += implant.item.price.price + + fitting_price = module_price + drone_price + fighter_price + cargo_price + booster_price + implant_price + total_price = ship_price + fitting_price + + self.labelPriceShip.SetLabel("%s ISK" % formatAmount(ship_price, 3, 3, 9, currency=True)) + self.labelPriceShip.SetToolTip(wx.ToolTip('{:,.2f}'.format(ship_price))) + + self.labelPriceFittings.SetLabel("%s ISK" % formatAmount(fitting_price, 3, 3, 9, currency=True)) + self.labelPriceFittings.SetToolTip(wx.ToolTip('{:,.2f}'.format(fitting_price))) + + self.labelPriceTotal.SetLabel("%s ISK" % formatAmount(total_price, 3, 3, 9, currency=True)) + self.labelPriceTotal.SetToolTip(wx.ToolTip('{:,.2f}'.format(total_price))) + + def processPrices(self, prices): + self.refreshPanelPrices(self.fit) + + self.labelEMStatus.SetLabel("") + self.panel.Layout() + + +PriceViewMinimal.register() diff --git a/gui/builtinStatsViews/rechargeViewFull.py b/gui/builtinStatsViews/rechargeViewFull.py index 21399602e..18af61d73 100644 --- a/gui/builtinStatsViews/rechargeViewFull.py +++ b/gui/builtinStatsViews/rechargeViewFull.py @@ -88,14 +88,19 @@ class RechargeViewFull(StatsView): box = wx.BoxSizer(wx.HORIZONTAL) box.Add(lbl, 0, wx.EXPAND) - box.Add(wx.StaticText(contentPanel, wx.ID_ANY, " HP/s"), 0, wx.EXPAND) + + unitlbl = wx.StaticText(contentPanel, wx.ID_ANY, " EHP/s") + setattr(self, "unitLabelTank%s%s" % (stability.capitalize(), tankTypeCap), unitlbl) + box.Add(unitlbl, 0, wx.EXPAND) sizerTankStats.Add(box, 0, wx.ALIGN_CENTRE) contentPanel.Layout() def refreshPanel(self, fit): - # If we did anything intresting, we'd update our labels to reflect the new fit's stats here + # If we did anything interesting, we'd update our labels to reflect the new fit's stats here + + unit = " EHP/s" if self.parent.nameViewMap['resistancesViewFull'].showEffective else " HP/s" for stability in ("reinforced", "sustained"): if stability == "reinforced" and fit is not None: @@ -107,6 +112,8 @@ class RechargeViewFull(StatsView): for name in ("shield", "armor", "hull"): lbl = getattr(self, "labelTank%s%sActive" % (stability.capitalize(), name.capitalize())) + unitlbl = getattr(self, "unitLabelTank%s%sActive" % (stability.capitalize(), name.capitalize())) + unitlbl.SetLabel(unit) if tank is not None: lbl.SetLabel("%.1f" % tank["%sRepair" % name]) else: @@ -116,6 +123,8 @@ class RechargeViewFull(StatsView): label = getattr(self, "labelTankSustainedShieldPassive") value = fit.effectiveTank["passiveShield"] if self.effective else fit.tank["passiveShield"] label.SetLabel(formatAmount(value, 3, 0, 9)) + unitlbl = getattr(self, "unitLabelTankSustainedShieldPassive") + unitlbl.SetLabel(unit) else: value = 0 diff --git a/gui/builtinStatsViews/targetingMiscViewMinimal.py b/gui/builtinStatsViews/targetingMiscViewMinimal.py index 4dffc9e40..df0d9f70a 100644 --- a/gui/builtinStatsViews/targetingMiscViewMinimal.py +++ b/gui/builtinStatsViews/targetingMiscViewMinimal.py @@ -242,7 +242,6 @@ class TargetingMiscViewMinimal(StatsView): # forces update of probe size, since this stat is used by both sig radius and sensor str if labelName == "labelFullSigRadius": - print "labelName" if fit: label.SetToolTip(wx.ToolTip("Probe Size: %.3f" % (fit.probeSize or 0))) else: diff --git a/gui/builtinViewColumns/price.py b/gui/builtinViewColumns/price.py index 194aa594d..3797052a2 100644 --- a/gui/builtinViewColumns/price.py +++ b/gui/builtinViewColumns/price.py @@ -22,7 +22,7 @@ import wx from eos.saveddata.cargo import Cargo from eos.saveddata.drone import Drone -from service.market import Market +from service.price import Price as ServicePrice from gui.viewColumn import ViewColumn from gui.bitmapLoader import BitmapLoader from gui.utils.numberFormatter import formatAmount @@ -41,13 +41,14 @@ class Price(ViewColumn): if stuff.item is None or stuff.item.group.name == "Ship Modifiers": return "" - sMkt = Market.getInstance() - price = sMkt.getPriceNow(stuff.item.ID) + if hasattr(stuff, "isEmpty"): + if stuff.isEmpty: + return "" - if not price or not price.price or not price.isValid: - return False + price = stuff.item.price.price - price = price.price # Set new price variable with what we need + if not price: + return "" if isinstance(stuff, Drone) or isinstance(stuff, Cargo): price *= stuff.amount @@ -55,10 +56,10 @@ class Price(ViewColumn): return formatAmount(price, 3, 3, 9, currency=True) def delayedText(self, mod, display, colItem): - sMkt = Market.getInstance() + sPrice = ServicePrice.getInstance() def callback(item): - price = sMkt.getPriceNow(item.ID) + price = item.item.price text = formatAmount(price.price, 3, 3, 9, currency=True) if price.price else "" if price.failed: text += " (!)" @@ -66,7 +67,7 @@ class Price(ViewColumn): display.SetItem(colItem) - sMkt.waitForPrice(mod.item, callback) + sPrice.getPrices([mod.item], callback, True) def getImageId(self, mod): return -1 diff --git a/gui/builtinViewColumns/state.py b/gui/builtinViewColumns/state.py index aa3f79ebe..c99ae2947 100644 --- a/gui/builtinViewColumns/state.py +++ b/gui/builtinViewColumns/state.py @@ -68,11 +68,17 @@ class State(ViewColumn): "gui") elif isinstance(stuff, Fit): fitID = self.mainFrame.getActiveFit() - projectionInfo = stuff.getProjectionInfo(fitID) - if projectionInfo is None: + # Can't use isinstance here due to being prevented from importing CommandView. + # So we do the next best thing and compare Name of class. + if self.fittingView.__class__.__name__ == "CommandView": + info = stuff.getCommandInfo(fitID) + else: + info = stuff.getProjectionInfo(fitID) + + if info is None: return -1 - if projectionInfo.active: + if info.active: return generic_active return generic_inactive elif isinstance(stuff, Implant) and stuff.character: diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index 0b64d9d53..1aebfe8ec 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -32,7 +32,6 @@ from eos.saveddata.module import Module, Slot, Rack from gui.builtinViewColumns.state import State from gui.bitmapLoader import BitmapLoader import gui.builtinViews.emptyView -from gui.utils.exportHtml import exportHtml from logbook import Logger from gui.chromeTabs import EVT_NOTEBOOK_PAGE_CHANGED @@ -268,9 +267,7 @@ class FittingView(d.Display): We also refresh the fit of the new current page in case delete fit caused change in stats (projected) """ - fitID = event.fitID - - if fitID == self.getActiveFit(): + if event.fitID == self.getActiveFit(): self.parent.DeletePage(self.parent.GetPageIndex(self)) try: @@ -279,7 +276,7 @@ class FittingView(d.Display): sFit.refreshFit(self.getActiveFit()) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID)) except wx._core.PyDeadObjectError: - pyfalog.warning("Caught dead object") + pyfalog.error("Caught dead object") pass event.Skip() @@ -336,7 +333,7 @@ class FittingView(d.Display): populate = sFit.appendModule(fitID, itemID) if populate is not None: self.slotsChanged() - wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID, action="modadd", typeID=itemID)) event.Skip() @@ -357,7 +354,7 @@ class FittingView(d.Display): if populate is not None: self.slotsChanged() - wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID)) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.activeFitID, action="moddel", typeID=module.item.ID)) def addModule(self, x, y, srcIdx): """Add a module from the market browser""" @@ -370,7 +367,8 @@ class FittingView(d.Display): if moduleChanged is None: # the new module doesn't fit in specified slot, try to simply append it wx.PostEvent(self.mainFrame, gui.marketBrowser.ItemSelected(itemID=srcIdx)) - wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit())) + + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit(), action="modadd", typeID=srcIdx)) def swapCargo(self, x, y, srcIdx): """Swap a module from cargo to fitting window""" @@ -381,10 +379,13 @@ class FittingView(d.Display): module = self.mods[dstRow] sFit = Fit.getInstance() + fit = sFit.getFit(self.activeFitID) + typeID = fit.cargo[srcIdx].item.ID + sFit.moveCargoToModule(self.mainFrame.getActiveFit(), module.modPosition, srcIdx, mstate.CmdDown() and module.isEmpty) - wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit())) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit(), action="modadd", typeID=typeID)) def swapItems(self, x, y, srcIdx): """Swap two modules in fitting window""" @@ -480,12 +481,11 @@ class FittingView(d.Display): # This only happens when turning on/off slot divisions self.populate(self.mods) self.refresh(self.mods) - - exportHtml.getInstance().refreshFittingHtml() + self.Refresh() self.Show(self.activeFitID is not None and self.activeFitID == event.fitID) except wx._core.PyDeadObjectError: - pyfalog.warning("Caught dead object") + pyfalog.error("Caught dead object") finally: event.Skip() @@ -611,12 +611,13 @@ class FittingView(d.Display): slotMap[slot] = fit.getSlotsFree(slot) < 0 font = (self.GetClassDefaultAttributes()).font + for i, mod in enumerate(self.mods): self.SetItemBackgroundColour(i, self.GetBackgroundColour()) # only consider changing color if we're dealing with a Module if type(mod) is Module: - if slotMap[mod.slot]: # Color too many modules as red + if slotMap[mod.slot] or getattr(mod, 'restrictionOverridden', None): # Color too many modules as red self.SetItemBackgroundColour(i, wx.Colour(204, 51, 51)) elif sFit.serviceFittingOptions["colorFitBySlot"]: # Color by slot it enabled self.SetItemBackgroundColour(i, self.slotColour(mod.slot)) diff --git a/gui/cargoView.py b/gui/cargoView.py index d997afa79..cd560c1c2 100644 --- a/gui/cargoView.py +++ b/gui/cargoView.py @@ -125,7 +125,7 @@ class CargoView(d.Display): if not mstate.CmdDown(): # if not copying, remove module sFit.removeModule(self.mainFrame.getActiveFit(), module.position) - wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit())) + wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=self.mainFrame.getActiveFit(), action="moddel", typeID=module.item.ID)) def fitChanged(self, event): sFit = Fit.getInstance() diff --git a/gui/characterEditor.py b/gui/characterEditor.py index 8006fa56a..c7862e817 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -20,6 +20,7 @@ # noinspection PyPackageRequirements import wx +from utils.floatspin import FloatSpin # noinspection PyPackageRequirements import wx.lib.newevent # noinspection PyPackageRequirements @@ -63,6 +64,28 @@ class CharacterTextValidor(BaseValidator): return False +class PlaceholderTextCtrl(wx.TextCtrl): + def __init__(self, *args, **kwargs): + self.default_text = kwargs.pop("placeholder", "") + kwargs["value"] = self.default_text + wx.TextCtrl.__init__(self, *args, **kwargs) + self.Bind(wx.EVT_SET_FOCUS, self.OnFocus) + self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + + def OnFocus(self, evt): + if self.GetValue() == self.default_text: + self.SetValue("") + evt.Skip() + + def OnKillFocus(self, evt): + if self.GetValue().strip() == "": + self.SetValue(self.default_text) + evt.Skip() + + def Reset(self): + self.SetValue(self.default_text) + + class CharacterEntityEditor(EntityEditor): def __init__(self, parent): EntityEditor.__init__(self, parent, "Character") @@ -252,10 +275,16 @@ class SkillTreeView(wx.Panel): pmainSizer = wx.BoxSizer(wx.VERTICAL) + hSizer = wx.BoxSizer(wx.HORIZONTAL) + self.clonesChoice = wx.Choice(self, wx.ID_ANY, style=0) i = self.clonesChoice.Append("Omega Clone", None) self.clonesChoice.SetSelection(i) - pmainSizer.Add(self.clonesChoice, 0, wx.ALL | wx.EXPAND, 5) + hSizer.Add(self.clonesChoice, 5, wx.ALL | wx.EXPAND, 5) + + self.searchInput = PlaceholderTextCtrl(self, wx.ID_ANY, placeholder="Search...") + hSizer.Add(self.searchInput, 1, wx.ALL | wx.EXPAND, 5) + self.searchInput.Bind(wx.EVT_TEXT, self.delaySearch) sChar = Character.getInstance() self.alphaClones = sChar.getAlphaCloneList() @@ -268,6 +297,12 @@ class SkillTreeView(wx.Panel): self.clonesChoice.Bind(wx.EVT_CHOICE, self.cloneChanged) + pmainSizer.Add(hSizer, 0, wx.EXPAND | wx.ALL, 5) + + # Set up timer for skill search + self.searchTimer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.populateSkillTreeSkillSearch, self.searchTimer) + tree = self.skillTreeListCtrl = wx.gizmos.TreeListCtrl(self, wx.ID_ANY, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT) pmainSizer.Add(tree, 1, wx.EXPAND | wx.ALL, 5) @@ -284,11 +319,19 @@ class SkillTreeView(wx.Panel): tree.SetColumnWidth(0, 500) + self.btnSecStatus = wx.Button(self, wx.ID_ANY, "Sec Status: {0:.2f}".format(char.secStatus or 0.0)) + self.btnSecStatus.Bind(wx.EVT_BUTTON, self.onSecStatus) + self.populateSkillTree() tree.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.expandLookup) tree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.scheduleMenu) + bSizerButtons = wx.BoxSizer(wx.HORIZONTAL) + + bSizerButtons.Add(self.btnSecStatus, 0, wx.ALL, 5) + pmainSizer.Add(bSizerButtons, 0, wx.EXPAND, 5) + # bind the Character selection event self.charEditor.entityEditor.Bind(wx.EVT_CHOICE, self.charChanged) self.charEditor.Bind(GE.CHAR_LIST_UPDATED, self.populateSkillTree) @@ -322,20 +365,58 @@ class SkillTreeView(wx.Panel): self.Layout() + def onSecStatus(self, event): + sChar = Character.getInstance() + char = self.charEditor.entityEditor.getActiveEntity() + myDlg = SecStatusDialog(self, char.secStatus or 0.0) + res = myDlg.ShowModal() + if res == wx.ID_OK: + value = myDlg.floatSpin.GetValue() + sChar.setSecStatus(char, value) + self.btnSecStatus.SetLabel("Sec Status: {0:.2f}".format(value)) + myDlg.Destroy() + + def delaySearch(self, evt): + if self.searchInput.GetValue() == "" or self.searchInput.GetValue() == self.searchInput.default_text: + self.populateSkillTree() + else: + self.searchTimer.Stop() + self.searchTimer.Start(150, True) # 150ms + def cloneChanged(self, event): sChar = Character.getInstance() sChar.setAlphaClone(self.charEditor.entityEditor.getActiveEntity(), event.ClientData) self.populateSkillTree() def charChanged(self, event=None): + self.searchInput.Reset() char = self.charEditor.entityEditor.getActiveEntity() for i in range(self.clonesChoice.GetCount()): cloneID = self.clonesChoice.GetClientData(i) if char.alphaCloneID == cloneID: self.clonesChoice.SetSelection(i) + self.btnSecStatus.SetLabel("Sec Status: {0:.2f}".format(char.secStatus or 0.0)) + self.populateSkillTree(event) + def populateSkillTreeSkillSearch(self, event=None): + sChar = Character.getInstance() + char = self.charEditor.entityEditor.getActiveEntity() + search = self.searchInput.GetLineText(0) + + root = self.root + tree = self.skillTreeListCtrl + tree.DeleteChildren(root) + + for id, name in sChar.getSkillsByName(search): + iconId = self.skillBookImageId + childId = tree.AppendItem(root, name, iconId, data=wx.TreeItemData(('skill', id))) + level, dirty = sChar.getSkillLevel(char.ID, id) + tree.SetItemText(childId, "Level %d" % int(level) if isinstance(level, float) else level, 1) + if dirty: + tree.SetItemTextColour(childId, wx.BLUE) + def populateSkillTree(self, event=None): sChar = Character.getInstance() char = self.charEditor.entityEditor.getActiveEntity() @@ -343,8 +424,10 @@ class SkillTreeView(wx.Panel): if char.name in ("All 0", "All 5"): self.clonesChoice.Disable() + self.btnSecStatus.Disable() else: self.clonesChoice.Enable() + self.btnSecStatus.Enable() groups = sChar.getSkillGroups() imageId = self.skillBookImageId @@ -354,7 +437,7 @@ class SkillTreeView(wx.Panel): for id, name in groups: childId = tree.AppendItem(root, name, imageId) - tree.SetPyData(childId, id) + tree.SetPyData(childId, ('group', id)) tree.AppendItem(childId, "dummy") if id in dirtyGroups: tree.SetItemTextColour(childId, wx.BLUE) @@ -374,11 +457,12 @@ class SkillTreeView(wx.Panel): # Get the real intrestin' stuff sChar = Character.getInstance() char = self.charEditor.entityEditor.getActiveEntity() - for id, name in sChar.getSkills(tree.GetPyData(root)): + data = tree.GetPyData(root) + for id, name in sChar.getSkills(data[1]): iconId = self.skillBookImageId - childId = tree.AppendItem(root, name, iconId, data=wx.TreeItemData(id)) + childId = tree.AppendItem(root, name, iconId, data=wx.TreeItemData(('skill', id))) level, dirty = sChar.getSkillLevel(char.ID, id) - tree.SetItemText(childId, "Level %d" % level if isinstance(level, int) else level, 1) + tree.SetItemText(childId, "Level %d" % int(level) if isinstance(level, float) else level, 1) if dirty: tree.SetItemTextColour(childId, wx.BLUE) @@ -395,11 +479,12 @@ class SkillTreeView(wx.Panel): char = self.charEditor.entityEditor.getActiveEntity() sMkt = Market.getInstance() + id = self.skillTreeListCtrl.GetPyData(item)[1] if char.name not in ("All 0", "All 5"): - self.levelChangeMenu.selection = sMkt.getItem(self.skillTreeListCtrl.GetPyData(item)) + self.levelChangeMenu.selection = sMkt.getItem(id) self.PopupMenu(self.levelChangeMenu) else: - self.statsMenu.selection = sMkt.getItem(self.skillTreeListCtrl.GetPyData(item)) + self.statsMenu.selection = sMkt.getItem(id) self.PopupMenu(self.statsMenu) def changeLevel(self, event): @@ -408,26 +493,54 @@ class SkillTreeView(wx.Panel): sChar = Character.getInstance() char = self.charEditor.entityEditor.getActiveEntity() selection = self.skillTreeListCtrl.GetSelection() - skillID = self.skillTreeListCtrl.GetPyData(selection) + dataType, skillID = self.skillTreeListCtrl.GetPyData(selection) if level is not None: - self.skillTreeListCtrl.SetItemText(selection, "Level %d" % level if isinstance(level, int) else level, 1) sChar.changeLevel(char.ID, skillID, level, persist=True) elif event.Id == self.revertID: sChar.revertLevel(char.ID, skillID) elif event.Id == self.saveID: sChar.saveSkill(char.ID, skillID) - self.skillTreeListCtrl.SetItemTextColour(selection, None) + # After saving the skill, we need to update not just the selected skill, but all open skills due to strict skill + # level setting. We don't want to refresh tree, as that will lose all expanded categories and users location + # within the tree. Thus, we loop through the tree and refresh the info. + # @todo: when collapsing branch, remove the data. This will make this loop more performant + child, cookie = self.skillTreeListCtrl.GetFirstChild(self.root) + + def _setTreeSkillLevel(treeItem, skillID): + lvl, dirty = sChar.getSkillLevel(char.ID, skillID) + self.skillTreeListCtrl.SetItemText(treeItem, + "Level {}".format(int(lvl)) if not isinstance(lvl, basestring) else lvl, + 1) + if not dirty: + self.skillTreeListCtrl.SetItemTextColour(treeItem, None) + + while child.IsOk(): + # child = Skill category + dataType, id = self.skillTreeListCtrl.GetPyData(child) + if dataType == 'skill': + _setTreeSkillLevel(child, id) + else: + grand, cookie2 = self.skillTreeListCtrl.GetFirstChild(child) + + while grand.IsOk(): + if self.skillTreeListCtrl.GetItemText(grand) != "dummy": + _, skillID = self.skillTreeListCtrl.GetPyData(grand) + _setTreeSkillLevel(grand, skillID) + grand, cookie2 = self.skillTreeListCtrl.GetNextChild(child, cookie2) + + child, cookie = self.skillTreeListCtrl.GetNextChild(self.root, cookie) dirtySkills = sChar.getDirtySkills(char.ID) dirtyGroups = set([skill.item.group.ID for skill in dirtySkills]) parentID = self.skillTreeListCtrl.GetItemParent(selection) - groupID = self.skillTreeListCtrl.GetPyData(parentID) + parent = self.skillTreeListCtrl.GetPyData(parentID) - if groupID not in dirtyGroups: - self.skillTreeListCtrl.SetItemTextColour(parentID, None) + if parent: + if parent[1] in dirtyGroups: + self.skillTreeListCtrl.SetItemTextColour(parentID, None) event.Skip() @@ -657,14 +770,19 @@ class APIView(wx.Panel): def fetchSkills(self, event): charName = self.charChoice.GetString(self.charChoice.GetSelection()) if charName: - try: - sChar = Character.getInstance() - activeChar = self.charEditor.entityEditor.getActiveEntity() - sChar.apiFetch(activeChar.ID, charName) - self.stStatus.SetLabel("Successfully fetched %s\'s skills from EVE API." % charName) - except Exception, e: - pyfalog.error("Unable to retrieve {0}\'s skills. Error message:\n{1}", charName, e) - self.stStatus.SetLabel("Unable to retrieve %s\'s skills. Error message:\n%s" % (charName, e)) + sChar = Character.getInstance() + activeChar = self.charEditor.entityEditor.getActiveEntity() + sChar.apiFetch(activeChar.ID, charName, self.__fetchCallback) + self.stStatus.SetLabel("Getting skills for {}".format(charName)) + + def __fetchCallback(self, e=None): + charName = self.charChoice.GetString(self.charChoice.GetSelection()) + if e is None: + self.stStatus.SetLabel("Successfully fetched {}\'s skills from EVE API.".format(charName)) + else: + exc_type, exc_obj, exc_trace = e + pyfalog.error("Unable to retrieve {0}\'s skills. Error message:\n{1}".format(charName, exc_obj)) + self.stStatus.SetLabel("Unable to retrieve {}\'s skills. Error message:\n{}".format(charName, exc_obj)) class SaveCharacterAs(wx.Dialog): @@ -695,3 +813,30 @@ class SaveCharacterAs(wx.Dialog): event.Skip() self.Close() + + +class SecStatusDialog(wx.Dialog): + + def __init__(self, parent, sec): + wx.Dialog.__init__(self, parent, title="Set Security Status", size=(275, 175)) + + self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize) + + bSizer1 = wx.BoxSizer(wx.VERTICAL) + + self.m_staticText1 = wx.StaticText(self, wx.ID_ANY, + u"Security Status is used in some CONCORD hull calculations; you can set the characters security status here", + wx.DefaultPosition, wx.DefaultSize, 0) + self.m_staticText1.Wrap(-1) + bSizer1.Add(self.m_staticText1, 1, wx.ALL | wx.EXPAND, 5) + + self.floatSpin = FloatSpin(self, value=sec, min_val=-5.0, max_val=5.0, increment=0.1, digits=2, size=(100, -1)) + bSizer1.Add(self.floatSpin, 0, wx.ALIGN_CENTER | wx.ALL, 5) + + btnOk = wx.Button(self, wx.ID_OK) + bSizer1.Add(btnOk, 0, wx.ALIGN_RIGHT | wx.ALL, 5) + + self.SetSizer(bSizer1) + self.Layout() + + self.Centre(wx.BOTH) diff --git a/gui/characterSelection.py b/gui/characterSelection.py index 3ee531d8f..e27fd62e2 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -25,6 +25,7 @@ import gui.mainFrame from service.character import Character from service.fit import Fit from logbook import Logger + pyfalog = Logger(__name__) @@ -50,6 +51,7 @@ class CharacterSelection(wx.Panel): self.redSkills = BitmapLoader.getBitmap("skillRed_big", "gui") self.greenSkills = BitmapLoader.getBitmap("skillGreen_big", "gui") self.refresh = BitmapLoader.getBitmap("refresh", "gui") + self.needsSkills = False self.btnRefresh = wx.BitmapButton(self, wx.ID_ANY, self.refresh) size = self.btnRefresh.GetSize() @@ -67,6 +69,8 @@ class CharacterSelection(wx.Panel): self.skillReqsStaticBitmap.SetBitmap(self.cleanSkills) mainSizer.Add(self.skillReqsStaticBitmap, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3) + self.skillReqsStaticBitmap.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu) + self.Bind(wx.EVT_CHOICE, self.charChanged) self.mainFrame.Bind(GE.CHAR_LIST_UPDATED, self.refreshCharacterList) self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) @@ -75,6 +79,42 @@ class CharacterSelection(wx.Panel): self.charChoice.Enable(False) + def OnContextMenu(self, event): + sFit = Fit.getInstance() + fit = sFit.getFit(self.mainFrame.getActiveFit()) + + if not fit or not self.needsSkills: + return + + pos = wx.GetMousePosition() + pos = self.ScreenToClient(pos) + + menu = wx.Menu() + + grantItem = menu.Append(wx.ID_ANY, "Grant Missing Skills") + self.Bind(wx.EVT_MENU, self.grantMissingSkills, grantItem) + + self.PopupMenu(menu, pos) + + event.Skip() + + def grantMissingSkills(self, evt): + charID = self.getActiveCharacter() + sChar = Character.getInstance() + + skillsMap = {} + for item, stuff in self.reqs.iteritems(): + for things in stuff.values(): + if things[1] not in skillsMap: + skillsMap[things[1]] = things[0] + elif things[0] > skillsMap[things[1]]: + skillsMap[things[1]] = things[0] + + for skillID, level in skillsMap.iteritems(): + sChar.changeLevel(charID, skillID, level, ifHigher=True) + + self.refreshCharacterList() + def getActiveCharacter(self): selection = self.charChoice.GetCurrentSelection() return self.charChoice.GetClientData(selection) if selection is not -1 else None @@ -109,16 +149,24 @@ class CharacterSelection(wx.Panel): event.Skip() def refreshApi(self, event): + self.btnRefresh.Enable(False) sChar = Character.getInstance() ID, key, charName, chars = sChar.getApiDetails(self.getActiveCharacter()) if charName: - try: - sChar.apiFetch(self.getActiveCharacter(), charName) - except Exception as e: - # can we do a popup, notifying user of API error? - pyfalog.error("API fetch error") - pyfalog.error(e) - self.refreshCharacterList() + sChar.apiFetch(self.getActiveCharacter(), charName, self.refreshAPICallback) + + def refreshAPICallback(self, e=None): + self.btnRefresh.Enable(True) + if e is None: + self.refreshCharacterList() + else: + exc_type, exc_obj, exc_trace = e + pyfalog.warn("Error fetching API information for character") + pyfalog.warn(exc_obj) + + wx.MessageBox( + "Error fetching API information, please check your API details in the character editor and try again later", + "Error", wx.ICON_ERROR | wx.STAY_ON_TOP) def charChanged(self, event): fitID = self.mainFrame.getActiveFit() @@ -152,31 +200,38 @@ class CharacterSelection(wx.Panel): return False def fitChanged(self, event): + """ + When fit is changed, or new fit is selected + """ self.charChoice.Enable(event.fitID is not None) choice = self.charChoice sFit = Fit.getInstance() currCharID = choice.GetClientData(choice.GetCurrentSelection()) fit = sFit.getFit(event.fitID) newCharID = fit.character.ID if fit is not None else None + if event.fitID is None: self.skillReqsStaticBitmap.SetBitmap(self.cleanSkills) self.skillReqsStaticBitmap.SetToolTipString("No active fit") else: sCharacter = Character.getInstance() - reqs = sCharacter.checkRequirements(fit) + self.reqs = sCharacter.checkRequirements(fit) + sCharacter.skillReqsDict = {'charname': fit.character.name, 'skills': []} - if len(reqs) == 0: + if len(self.reqs) == 0: + self.needsSkills = False tip = "All skill prerequisites have been met" self.skillReqsStaticBitmap.SetBitmap(self.greenSkills) else: + self.needsSkills = True tip = "Skills required:\n" condensed = sFit.serviceFittingOptions["compactSkills"] if condensed: - dict_ = self._buildSkillsTooltipCondensed(reqs, skillsMap={}) + dict_ = self._buildSkillsTooltipCondensed(self.reqs, skillsMap={}) for key in sorted(dict_): tip += "%s: %d\n" % (key, dict_[key]) else: - tip += self._buildSkillsTooltip(reqs) + tip += self._buildSkillsTooltip(self.reqs) self.skillReqsStaticBitmap.SetBitmap(self.redSkills) self.skillReqsStaticBitmap.SetToolTipString(tip.strip()) @@ -186,7 +241,8 @@ class CharacterSelection(wx.Panel): elif currCharID != newCharID: self.selectChar(newCharID) - self.charChanged(None) + if not fit.calculated: + self.charChanged(None) event.Skip() diff --git a/gui/commandView.py b/gui/commandView.py index 38dec46c9..6ba38859b 100644 --- a/gui/commandView.py +++ b/gui/commandView.py @@ -25,6 +25,7 @@ import gui.globalEvents as GE import gui.droneView from gui.builtinViewColumns.state import State from gui.contextMenu import ContextMenu +from gui.builtinContextMenus.commandFits import CommandFits from service.fit import Fit from eos.saveddata.drone import Drone as es_Drone @@ -56,13 +57,14 @@ class CommandViewDrop(wx.PyDropTarget): class CommandView(d.Display): - DEFAULT_COLS = ["Base Name"] + DEFAULT_COLS = ["State", "Base Name"] def __init__(self, parent): d.Display.__init__(self, parent, style=wx.LC_SINGLE_SEL | wx.BORDER_NONE) self.lastFitId = None + self.mainFrame.Bind(GE.FIT_CHANGED, CommandFits.populateFits) self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) self.Bind(wx.EVT_LEFT_DOWN, self.click) self.Bind(wx.EVT_RIGHT_DOWN, self.click) @@ -194,13 +196,13 @@ class CommandView(d.Display): fitSrcContext = "commandFit" fitItemContext = item.name context = ((fitSrcContext, fitItemContext),) - context += ("command",), + context += ("commandView",), menu = ContextMenu.getMenu((item,), *context) elif sel == -1: fitID = self.mainFrame.getActiveFit() if fitID is None: return - context = (("command",),) + context = (("commandView",),) menu = ContextMenu.getMenu([], *context) if menu is not None: self.PopupMenu(menu) diff --git a/gui/contextMenu.py b/gui/contextMenu.py index a35c73282..155a42787 100644 --- a/gui/contextMenu.py +++ b/gui/contextMenu.py @@ -205,4 +205,6 @@ from gui.builtinContextMenus import ( # noqa: E402,F401 metaSwap, implantSets, fighterAbilities, + commandFits, + tabbedFits ) diff --git a/gui/droneView.py b/gui/droneView.py index e506c278d..b075cfee0 100644 --- a/gui/droneView.py +++ b/gui/droneView.py @@ -202,7 +202,7 @@ class DroneView(Display): fit = sFit.getFit(fitID) - if fit.isStructure: + if not fit or fit.isStructure: return trigger = sFit.addDrone(fitID, event.itemID) diff --git a/gui/errorDialog.py b/gui/errorDialog.py index 469eb9e63..f0a8c3bce 100644 --- a/gui/errorDialog.py +++ b/gui/errorDialog.py @@ -17,23 +17,40 @@ # along with pyfa. If not, see . # =============================================================================== -import wx +import platform import sys -import gui.utils.fonts as fonts -import config + +# noinspection PyPackageRequirements +import wx + +try: + import config +except: + config = None + +try: + import sqlalchemy + + sqlalchemy_version = sqlalchemy.__version__ +except: + sqlalchemy_version = "Unknown" + +try: + from logbook import __version__ as logbook_version +except: + logbook_version = "Unknown" class ErrorFrame(wx.Frame): + def __init__(self, exception=None, tb=None, error_title='Error!'): + v = sys.version_info - def __init__(self, exception, tb): - wx.Frame.__init__(self, None, id=wx.ID_ANY, title="pyfa error", pos=wx.DefaultPosition, size=wx.Size(500, 400), + wx.Frame.__init__(self, None, id=wx.ID_ANY, title="pyfa error", pos=wx.DefaultPosition, size=wx.Size(500, 600), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER | wx.STAY_ON_TOP) - desc = "pyfa has experienced an unexpected error. Below is the " \ - "Traceback that contains crucial information about how this " \ - "error was triggered. Please contact the developers with " \ - "the information provided through the EVE Online forums " \ - "or file a GitHub issue." + desc = "pyfa has experienced an unexpected issue. Below is a message that contains crucial\n" \ + "information about how this was triggered. Please contact the developers with the\n" \ + "information provided through the EVE Online forums or file a GitHub issue." self.SetSizeHintsSz(wx.DefaultSize, wx.DefaultSize) @@ -43,47 +60,78 @@ class ErrorFrame(wx.Frame): mainSizer = wx.BoxSizer(wx.VERTICAL) headSizer = wx.BoxSizer(wx.HORIZONTAL) - self.headingText = wx.StaticText(self, wx.ID_ANY, "Error!", wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_CENTRE) - self.headingText.SetFont(wx.Font(14, 74, 90, 92, False)) + headingText = wx.StaticText(self, wx.ID_ANY, error_title, wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_CENTRE) + headingText.SetFont(wx.Font(14, 74, 90, 92, False)) - headSizer.Add(self.headingText, 1, wx.ALL, 5) + headSizer.Add(headingText, 1, wx.ALL, 5) mainSizer.Add(headSizer, 0, wx.EXPAND, 5) mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.EXPAND | wx.ALL, 5) - descSizer = wx.BoxSizer(wx.HORIZONTAL) - self.descText = wx.TextCtrl(self, wx.ID_ANY, desc, wx.DefaultPosition, wx.DefaultSize, - wx.TE_AUTO_URL | wx.TE_MULTILINE | wx.TE_READONLY | wx.BORDER_NONE | wx.TRANSPARENT_WINDOW) - self.descText.SetFont(wx.Font(fonts.BIG, wx.SWISS, wx.NORMAL, wx.NORMAL)) - descSizer.Add(self.descText, 1, wx.ALL, 5) - mainSizer.Add(descSizer, 1, wx.EXPAND, 5) + box = wx.BoxSizer(wx.VERTICAL) + mainSizer.Add(box, 0, wx.EXPAND | wx.ALIGN_TOP) - self.eveForums = wx.HyperlinkCtrl(self, wx.ID_ANY, "EVE Forums Thread", "https://forums.eveonline.com/default.aspx?g=posts&t=466425", - wx.DefaultPosition, wx.DefaultSize, wx.HL_DEFAULT_STYLE) + descText = wx.StaticText(self, wx.ID_ANY, desc) + box.Add(descText, 1, wx.ALL, 5) - mainSizer.Add(self.eveForums, 0, wx.ALL, 2) + github = wx.HyperlinkCtrl(self, wx.ID_ANY, "Github", "https://github.com/pyfa-org/Pyfa/issues", + wx.DefaultPosition, wx.DefaultSize, wx.HL_DEFAULT_STYLE) + box.Add(github, 0, wx.ALL, 5) - self.eveForums = wx.HyperlinkCtrl(self, wx.ID_ANY, "Github Issues", "https://github.com/pyfa-org/Pyfa/issues", - wx.DefaultPosition, wx.DefaultSize, wx.HL_DEFAULT_STYLE) - - mainSizer.Add(self.eveForums, 0, wx.ALL, 2) + eveForums = wx.HyperlinkCtrl(self, wx.ID_ANY, "EVE Forums", "https://forums.eveonline.com/default.aspx?g=posts&t=466425", + wx.DefaultPosition, wx.DefaultSize, wx.HL_DEFAULT_STYLE) + box.Add(eveForums, 0, wx.ALL, 5) # mainSizer.AddSpacer((0, 5), 0, wx.EXPAND, 5) - self.errorTextCtrl = wx.TextCtrl(self, wx.ID_ANY, "", wx.DefaultPosition, wx.DefaultSize, wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_DONTWRAP) - self.errorTextCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL)) - mainSizer.Add(self.errorTextCtrl, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 5) + errorTextCtrl = wx.TextCtrl(self, wx.ID_ANY, "", wx.DefaultPosition, (-1, 400), wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_DONTWRAP) + errorTextCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL)) + mainSizer.Add(errorTextCtrl, 0, wx.EXPAND | wx.ALL | wx.ALIGN_CENTER, 5) - self.errorTextCtrl.AppendText("pyfa root: ") - self.errorTextCtrl.AppendText(config.pyfaPath or "Unknown") - self.errorTextCtrl.AppendText('\n') - self.errorTextCtrl.AppendText("save path: ") - self.errorTextCtrl.AppendText(config.savePath or "Unknown") - self.errorTextCtrl.AppendText('\n') - self.errorTextCtrl.AppendText("fs encoding: ") - self.errorTextCtrl.AppendText(sys.getfilesystemencoding()) - self.errorTextCtrl.AppendText('\n\n') - self.errorTextCtrl.AppendText(tb) + try: + errorTextCtrl.AppendText("OS version: \t" + str(platform.platform())) + except: + errorTextCtrl.AppendText("OS version: Unknown") + errorTextCtrl.AppendText("\n") + + try: + errorTextCtrl.AppendText("Python: \t" + '{}.{}.{}'.format(v.major, v.minor, v.micro)) + except: + errorTextCtrl.AppendText("Python: Unknown") + errorTextCtrl.AppendText("\n") + + try: + errorTextCtrl.AppendText("wxPython: \t" + wx.VERSION_STRING) + except: + errorTextCtrl.AppendText("wxPython: Unknown") + errorTextCtrl.AppendText("\n") + + errorTextCtrl.AppendText("SQLAlchemy: \t" + str(sqlalchemy_version)) + errorTextCtrl.AppendText("\n") + + errorTextCtrl.AppendText("Logbook: \t" + str(logbook_version)) + errorTextCtrl.AppendText("\n") + + try: + errorTextCtrl.AppendText("pyfa version: {0} {1} - {2} {3}".format(config.version, config.tag, config.expansionName, config.expansionVersion)) + except: + errorTextCtrl.AppendText("pyfa version: Unknown") + errorTextCtrl.AppendText('\n') + + errorTextCtrl.AppendText("pyfa root: " + str(config.pyfaPath or "Unknown")) + errorTextCtrl.AppendText('\n') + errorTextCtrl.AppendText("save path: " + str(config.savePath or "Unknown")) + errorTextCtrl.AppendText('\n') + errorTextCtrl.AppendText("fs encoding: " + str(sys.getfilesystemencoding() or "Unknown")) + errorTextCtrl.AppendText('\n\n') + + errorTextCtrl.AppendText("EXCEPTION: " + str(exception or "Unknown")) + errorTextCtrl.AppendText('\n\n') + + if tb: + for line in tb: + errorTextCtrl.AppendText(line) + errorTextCtrl.Layout() self.SetSizer(mainSizer) mainSizer.Layout() diff --git a/gui/implantView.py b/gui/implantView.py index 86b9afd96..0044244a8 100644 --- a/gui/implantView.py +++ b/gui/implantView.py @@ -79,10 +79,13 @@ class ImplantView(wx.Panel): class ImplantDisplay(d.Display): - DEFAULT_COLS = ["State", - "attr:implantness", - "Base Icon", - "Base Name"] + DEFAULT_COLS = [ + "State", + "attr:implantness", + "Base Icon", + "Base Name", + "Price", + ] def __init__(self, parent): d.Display.__init__(self, parent, style=wx.LC_SINGLE_SEL | wx.BORDER_NONE) @@ -145,7 +148,7 @@ class ImplantDisplay(d.Display): fit = sFit.getFit(fitID) - if fit.isStructure: + if not fit or fit.isStructure: return trigger = sFit.addImplant(fitID, event.itemID) diff --git a/gui/itemStats.py b/gui/itemStats.py index 325793e86..2c9ce4a5d 100644 --- a/gui/itemStats.py +++ b/gui/itemStats.py @@ -43,6 +43,7 @@ from eos.saveddata.citadel import Citadel from eos.saveddata.fit import Fit from service.market import Market from service.attribute import Attribute +from service.price import Price as ServicePrice import gui.mainFrame from gui.bitmapLoader import BitmapLoader from gui.utils.numberFormatter import formatAmount @@ -190,6 +191,10 @@ class ItemStatsContainer(wx.Panel): self.reqs = ItemRequirements(self.nbContainer, stuff, item) self.nbContainer.AddPage(self.reqs, "Requirements") + if context == "Skill": + self.dependents = ItemDependents(self.nbContainer, stuff, item) + self.nbContainer.AddPage(self.dependents, "Dependents") + self.effects = ItemEffects(self.nbContainer, stuff, item) self.nbContainer.AddPage(self.effects, "Effects") @@ -530,6 +535,10 @@ class ItemParams(wx.Panel): class ItemCompare(wx.Panel): def __init__(self, parent, stuff, item, items, context=None): + # Start dealing with Price stuff to get that thread going + sPrice = ServicePrice.getInstance() + sPrice.getPrices(items, self.UpdateList) + wx.Panel.__init__(self, parent) mainSizer = wx.BoxSizer(wx.VERTICAL) @@ -586,11 +595,10 @@ class ItemCompare(wx.Panel): wx.DefaultSize, 0) bSizer.Add(self.toggleViewBtn, 0, wx.ALIGN_CENTER_VERTICAL) - if stuff is not None: - self.refreshBtn = wx.Button(self, wx.ID_ANY, u"Refresh", wx.DefaultPosition, wx.DefaultSize, - wx.BU_EXACTFIT) - bSizer.Add(self.refreshBtn, 0, wx.ALIGN_CENTER_VERTICAL) - self.refreshBtn.Bind(wx.EVT_BUTTON, self.RefreshValues) + self.refreshBtn = wx.Button(self, wx.ID_ANY, u"Refresh", wx.DefaultPosition, wx.DefaultSize, + wx.BU_EXACTFIT) + bSizer.Add(self.refreshBtn, 0, wx.ALIGN_CENTER_VERTICAL) + self.refreshBtn.Bind(wx.EVT_BUTTON, self.RefreshValues) mainSizer.Add(bSizer, 0, wx.ALIGN_RIGHT) @@ -605,7 +613,8 @@ class ItemCompare(wx.Panel): self.PopulateList(event.Column) self.Thaw() - def UpdateList(self): + def UpdateList(self, items=None): + # We do nothing with `items`, but it gets returned by the price service thread self.Freeze() self.paramList.ClearAll() self.PopulateList() @@ -623,7 +632,7 @@ class ItemCompare(wx.Panel): def processPrices(self, prices): for i, price in enumerate(prices): - self.paramList.SetStringItem(i, len(self.attrs) + 1, formatAmount(price.price, 3, 3, 9, currency=True)) + self.paramList.SetStringItem(i, len(self.attrs) + 1, formatAmount(price.value, 3, 3, 9, currency=True)) def PopulateList(self, sort=None): @@ -660,9 +669,6 @@ class ItemCompare(wx.Panel): self.paramList.InsertColumn(len(self.attrs) + 1, "Price") self.paramList.SetColumnWidth(len(self.attrs) + 1, 60) - sMkt = Market.getInstance() - sMkt.getPrices([x.ID for x in self.items], self.processPrices) - for item in self.items: i = self.paramList.InsertStringItem(sys.maxint, item.name) for x, attr in enumerate(self.attrs.keys()): @@ -678,6 +684,9 @@ class ItemCompare(wx.Panel): self.paramList.SetStringItem(i, x + 1, valueUnit) + # Add prices + self.paramList.SetStringItem(i, len(self.attrs) + 1, formatAmount(item.price.price, 3, 3, 9, currency=True)) + self.paramList.RefreshRows() self.Layout() @@ -757,6 +766,55 @@ class ItemRequirements(wx.Panel): self.skillIdHistory.append(skill.ID) +class ItemDependents(wx.Panel): + def __init__(self, parent, stuff, item): + wx.Panel.__init__(self, parent, style=wx.TAB_TRAVERSAL) + + # itemId is set by the parent. + self.romanNb = ["0", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"] + self.skillIdHistory = [] + mainSizer = wx.BoxSizer(wx.VERTICAL) + + self.reqTree = wx.TreeCtrl(self, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.NO_BORDER) + + mainSizer.Add(self.reqTree, 1, wx.ALL | wx.EXPAND, 0) + + self.SetSizer(mainSizer) + self.root = self.reqTree.AddRoot("WINRARZOR") + self.reqTree.SetPyData(self.root, None) + + self.imageList = wx.ImageList(16, 16) + self.reqTree.SetImageList(self.imageList) + skillBookId = self.imageList.Add(BitmapLoader.getBitmap("skill_small", "gui")) + + self.getFullSkillTree(item, self.root, skillBookId) + + self.Layout() + + def getFullSkillTree(self, parentSkill, parent, sbIconId): + levelToItems = {} + + for item, level in parentSkill.requiredFor.iteritems(): + if level not in levelToItems: + levelToItems[level] = [] + levelToItems[level].append(item) + + for x in sorted(levelToItems.keys()): + items = levelToItems[x] + items.sort(key=lambda x: x.name) + + child = self.reqTree.AppendItem(parent, "Level {}".format(self.romanNb[int(x)]), sbIconId) + for item in items: + + if item.icon: + bitmap = BitmapLoader.getBitmap(item.icon.iconFile, "icons") + itemIcon = self.imageList.Add(bitmap) if bitmap else -1 + else: + itemIcon = -1 + + self.reqTree.AppendItem(child, "{}".format(item.name), itemIcon) + + class ItemEffects(wx.Panel): def __init__(self, parent, stuff, item): wx.Panel.__init__(self, parent) @@ -1043,7 +1101,7 @@ class ItemAffectedBy(wx.Panel): container = {} for attrName in attributes.iterAfflictions(): # if value is 0 or there has been no change from original to modified, return - if attributes[attrName] == (attributes.getOriginal(attrName) or 0): + if attributes[attrName] == (attributes.getOriginal(attrName, 0)): continue for fit, afflictors in attributes.getAfflictions(attrName).iteritems(): @@ -1139,11 +1197,13 @@ class ItemAffectedBy(wx.Panel): if projected: displayStr += " (projected)" - if attrModifier == "s*": - attrModifier = "*" - penalized = "(penalized)" - else: - penalized = "" + penalized = "" + if '*' in attrModifier: + if 's' in attrModifier: + penalized += "(penalized)" + if 'r' in attrModifier: + penalized += "(resisted)" + attrModifier = "*" # this is the Module node, the attribute will be attached to this display = "%s %s %.2f %s" % (displayStr, attrModifier, attrAmount, penalized) @@ -1170,7 +1230,7 @@ class ItemAffectedBy(wx.Panel): container = {} for attrName in attributes.iterAfflictions(): # if value is 0 or there has been no change from original to modified, return - if attributes[attrName] == (attributes.getOriginal(attrName) or 0): + if attributes[attrName] == (attributes.getOriginal(attrName, 0)): continue for fit, afflictors in attributes.getAfflictions(attrName).iteritems(): @@ -1269,11 +1329,13 @@ class ItemAffectedBy(wx.Panel): else: attrIcon = self.imageList.Add(BitmapLoader.getBitmap("7_15", "icons")) - if attrModifier == "s*": - attrModifier = "*" - penalized = "(penalized)" - else: - penalized = "" + penalized = "" + if '*' in attrModifier: + if 's' in attrModifier: + penalized += "(penalized)" + if 'r' in attrModifier: + penalized += "(resisted)" + attrModifier = "*" attributes.append((attrName, (displayName if displayName != "" else attrName), attrModifier, attrAmount, penalized, attrIcon)) @@ -1376,22 +1438,21 @@ class ItemProperties(wx.Panel): else: attrName = name.title() value = getattr(self.item, name) - except Exception as e: + + index = self.paramList.InsertStringItem(sys.maxint, attrName) + # index = self.paramList.InsertImageStringItem(sys.maxint, attrName) + idNameMap[idCount] = attrName + self.paramList.SetItemData(index, idCount) + idCount += 1 + + valueUnit = str(value) + + self.paramList.SetStringItem(index, 1, valueUnit) + except: # TODO: Add logging to this. # We couldn't get a property for some reason. Skip it for now. - print(e) continue - index = self.paramList.InsertStringItem(sys.maxint, attrName) - # index = self.paramList.InsertImageStringItem(sys.maxint, attrName) - idNameMap[idCount] = attrName - self.paramList.SetItemData(index, idCount) - idCount += 1 - - valueUnit = str(value) - - self.paramList.SetStringItem(index, 1, valueUnit) - self.paramList.SortItems(lambda id1, id2: cmp(idNameMap[id1], idNameMap[id2])) self.paramList.RefreshRows() self.totalAttrsLabel.SetLabel("%d attributes. " % idCount) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 7af37d875..f81780b44 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -72,7 +72,7 @@ from service.update import Update 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 +from service.port import Port, IPortUser from service.settings import HTMLExportSettings from time import gmtime, strftime @@ -137,7 +137,7 @@ class OpenFitsThread(threading.Thread): wx.CallAfter(self.callback) -class MainFrame(wx.Frame): +class MainFrame(wx.Frame, IPortUser): __instance = None @classmethod @@ -255,7 +255,9 @@ class MainFrame(wx.Frame): # Remove any fits that cause exception when fetching (non-existent fits) for id in fits[:]: try: - sFit.getFit(id, basic=True) + fit = sFit.getFit(id, basic=True) + if fit is None: + fits.remove(id) except: fits.remove(id) @@ -419,8 +421,7 @@ class MainFrame(wx.Frame): format_ = dlg.GetFilterIndex() path = dlg.GetPath() if format_ == 0: - sPort = Port.getInstance() - output = sPort.exportXml(None, fit) + output = Port.exportXml(None, fit) if '.' not in os.path.basename(path): path += ".xml" else: @@ -522,6 +523,9 @@ class MainFrame(wx.Frame): # Clipboard exports self.Bind(wx.EVT_MENU, self.exportToClipboard, id=wx.ID_COPY) + # Fitting Restrictions + self.Bind(wx.EVT_MENU, self.toggleIgnoreRestriction, id=menuBar.toggleIgnoreRestrictionID) + # Graphs self.Bind(wx.EVT_MENU, self.openGraphFrame, id=menuBar.graphFrameId) @@ -582,6 +586,24 @@ class MainFrame(wx.Frame): atable = wx.AcceleratorTable(actb) self.SetAcceleratorTable(atable) + def toggleIgnoreRestriction(self, event): + + sFit = Fit.getInstance() + fitID = self.getActiveFit() + fit = sFit.getFit(fitID) + + if not fit.ignoreRestrictions: + dlg = wx.MessageDialog(self, "Are you sure you wish to ignore fitting restrictions for the " + "current fit? This could lead to wildly inaccurate results and possible errors.", "Confirm", wx.YES_NO | wx.ICON_QUESTION) + else: + dlg = wx.MessageDialog(self, "Re-enabling fitting restrictions for this fit will also remove any illegal items " + "from the fit. Do you want to continue?", "Confirm", wx.YES_NO | wx.ICON_QUESTION) + result = dlg.ShowModal() == wx.ID_YES + dlg.Destroy() + if result: + sFit.toggleRestrictionIgnore(fitID) + wx.PostEvent(self, GE.FitChanged(fitID=fitID)) + def eveFittings(self, event): dlg = CrestFittings(self) dlg.Show() @@ -651,11 +673,11 @@ class MainFrame(wx.Frame): dlg.Show() def toggleOverrides(self, event): - ModifiedAttributeDict.OVERRIDES = not ModifiedAttributeDict.OVERRIDES + ModifiedAttributeDict.overrides_enabled = not ModifiedAttributeDict.overrides_enabled wx.PostEvent(self, GE.FitChanged(fitID=self.getActiveFit())) menu = self.GetMenuBar() menu.SetLabel(menu.toggleOverridesId, - "Turn Overrides Off" if ModifiedAttributeDict.OVERRIDES else "Turn Overrides On") + "Turn Overrides Off" if ModifiedAttributeDict.overrides_enabled else "Turn Overrides On") def saveChar(self, event): sChr = Character.getInstance() @@ -790,7 +812,6 @@ class MainFrame(wx.Frame): def fileImportDialog(self, event): """Handles importing single/multiple EVE XML / EFT cfg fit files""" - sPort = Port.getInstance() dlg = wx.FileDialog( self, "Open One Or More Fitting Files", @@ -804,10 +825,10 @@ class MainFrame(wx.Frame): "Importing fits", " " * 100, # set some arbitrary spacing to create width in window parent=self, - style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME + style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL ) - self.progressDialog.message = None - sPort.importFitsThreaded(dlg.GetPaths(), self.fileImportCallback) + # self.progressDialog.message = None + Port.importFitsThreaded(dlg.GetPaths(), self) self.progressDialog.ShowModal() try: dlg.Destroy() @@ -839,9 +860,9 @@ class MainFrame(wx.Frame): "Backing up %d fits to: %s" % (max_, filePath), maximum=max_, parent=self, - style=wx.PD_APP_MODAL | wx.PD_ELAPSED_TIME, + style=wx.PD_CAN_ABORT | wx.PD_SMOOTH | wx.PD_ELAPSED_TIME | wx.PD_APP_MODAL ) - Port().backupFits(filePath, self.backupCallback) + Port.backupFits(filePath, self) self.progressDialog.ShowModal() def exportHtml(self, event): @@ -879,7 +900,19 @@ class MainFrame(wx.Frame): else: self.progressDialog.Update(info) - def fileImportCallback(self, action, data=None): + def on_port_process_start(self): + # flag for progress dialog. + self.__progress_flag = True + + def on_port_processing(self, action, data=None): + # 2017/03/29 NOTE: implementation like interface + wx.CallAfter( + self._on_port_processing, action, data + ) + + return self.__progress_flag + + def _on_port_processing(self, action, data): """ While importing fits from file, the logic calls back to this function to update progress bar to show activity. XML files can contain multiple @@ -893,22 +926,38 @@ class MainFrame(wx.Frame): 1: Replace message with data other: Close dialog and handle based on :action (-1 open fits, -2 display error) """ - - if action is None: - self.progressDialog.Pulse() - elif action == 1 and data != self.progressDialog.message: - self.progressDialog.message = data - self.progressDialog.Pulse(data) - else: + _message = None + if action & IPortUser.ID_ERROR: self.closeProgressDialog() - if action == -1: - self._openAfterImport(data) - elif action == -2: - dlg = wx.MessageDialog(self, - "The following error was generated\n\n%s\n\nBe aware that already processed fits were not saved" % data, - "Import Error", wx.OK | wx.ICON_ERROR) - if dlg.ShowModal() == wx.ID_OK: - return + _message = "Import Error" if action & IPortUser.PROCESS_IMPORT else "Export Error" + dlg = wx.MessageDialog(self, + "The following error was generated\n\n%s\n\nBe aware that already processed fits were not saved" % data, + _message, wx.OK | wx.ICON_ERROR) + # if dlg.ShowModal() == wx.ID_OK: + # return + dlg.ShowModal() + return + + # data is str + if action & IPortUser.PROCESS_IMPORT: + if action & IPortUser.ID_PULSE: + _message = () + # update message + elif action & IPortUser.ID_UPDATE: # and data != self.progressDialog.message: + _message = data + + if _message is not None: + self.__progress_flag, _unuse = self.progressDialog.Pulse(_message) + else: + self.closeProgressDialog() + if action & IPortUser.ID_DONE: + self._openAfterImport(data) + # data is tuple(int, str) + elif action & IPortUser.PROCESS_EXPORT: + if action & IPortUser.ID_DONE: + self.closeProgressDialog() + else: + self.__progress_flag, _unuse = self.progressDialog.Update(data[0], data[1]) def _openAfterImport(self, fits): if len(fits) > 0: @@ -917,7 +966,16 @@ class MainFrame(wx.Frame): wx.PostEvent(self, FitSelected(fitID=fit.ID)) wx.PostEvent(self.shipBrowser, Stage3Selected(shipID=fit.shipID, back=True)) else: - wx.PostEvent(self.shipBrowser, ImportSelected(fits=fits, back=True)) + fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name)) + results = [] + for fit in fits: + results.append(( + fit.ID, + fit.name, + fit.modifiedCoalesce, + fit.ship.item + )) + wx.PostEvent(self.shipBrowser, ImportSelected(fits=results, back=True)) def closeProgressDialog(self): # Windows apparently handles ProgressDialogs differently. We can diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index 00402a7a3..bade53d3d 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -22,6 +22,7 @@ import wx import config from service.character import Character +from service.fit import Fit import gui.graphFrame import gui.globalEvents as GE from gui.bitmapLoader import BitmapLoader @@ -57,6 +58,7 @@ class MainMenuBar(wx.MenuBar): self.attrEditorId = wx.NewId() self.toggleOverridesId = wx.NewId() self.importDatabaseDefaultsId = wx.NewId() + self.toggleIgnoreRestrictionID = wx.NewId() if 'wxMac' in wx.PlatformInfo and wx.VERSION >= (3, 0): wx.ID_COPY = wx.NewId() @@ -96,6 +98,8 @@ class MainMenuBar(wx.MenuBar): editMenu.Append(self.saveCharId, "Save Character") editMenu.Append(self.saveCharAsId, "Save Character As...") editMenu.Append(self.revertCharId, "Revert Character") + editMenu.AppendSeparator() + self.ignoreRestrictionItem = editMenu.Append(self.toggleIgnoreRestrictionID, "Ignore Fitting Restrictions") # Character menu windowMenu = wx.Menu() @@ -170,7 +174,6 @@ class MainMenuBar(wx.MenuBar): self.mainFrame.Bind(GE.FIT_CHANGED, self.fitChanged) def fitChanged(self, event): - pyfalog.debug("fitChanged triggered") enable = event.fitID is not None self.Enable(wx.ID_SAVEAS, enable) self.Enable(wx.ID_COPY, enable) @@ -185,4 +188,15 @@ class MainMenuBar(wx.MenuBar): self.Enable(self.saveCharAsId, char.isDirty) self.Enable(self.revertCharId, char.isDirty) + self.Enable(self.toggleIgnoreRestrictionID, enable) + + if event.fitID: + sFit = Fit.getInstance() + fit = sFit.getFit(event.fitID) + + if fit.ignoreRestrictions: + self.ignoreRestrictionItem.SetItemLabel("Enable Fitting Restrictions") + else: + self.ignoreRestrictionItem.SetItemLabel("Disable Fitting Restrictions") + event.Skip() diff --git a/gui/marketBrowser.py b/gui/marketBrowser.py index 4047507da..8321b7a8d 100644 --- a/gui/marketBrowser.py +++ b/gui/marketBrowser.py @@ -20,6 +20,7 @@ # noinspection PyPackageRequirements import wx from service.market import Market +from service.fit import Fit from service.attribute import Attribute from gui.display import Display import gui.PFSearchBox as SBox @@ -245,11 +246,15 @@ class ItemView(Display): self.marketBrowser = marketBrowser self.marketView = marketBrowser.marketView + # Set up timer for delaying search on every EVT_TEXT + self.searchTimer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.scheduleSearch, self.searchTimer) + # Make sure our search actually does interesting stuff self.marketBrowser.search.Bind(SBox.EVT_TEXT_ENTER, self.scheduleSearch) self.marketBrowser.search.Bind(SBox.EVT_SEARCH_BTN, self.scheduleSearch) self.marketBrowser.search.Bind(SBox.EVT_CANCEL_BTN, self.clearSearch) - self.marketBrowser.search.Bind(SBox.EVT_TEXT, self.scheduleSearch) + self.marketBrowser.search.Bind(SBox.EVT_TEXT, self.delaySearch) # Make sure WE do interesting stuff too self.Bind(wx.EVT_CONTEXT_MENU, self.contextMenu) @@ -264,6 +269,11 @@ class ItemView(Display): for itemID in self.sMkt.serviceMarketRecentlyUsedModules["pyfaMarketRecentlyUsedModules"]: self.recentlyUsedModules.add(self.sMkt.getItem(itemID)) + def delaySearch(self, evt): + sFit = Fit.getInstance() + self.searchTimer.Stop() + self.searchTimer.Start(sFit.serviceFittingOptions["marketSearchDelay"], True) # 150ms + def startDrag(self, event): row = self.GetFirstSelected() @@ -288,7 +298,7 @@ class ItemView(Display): for itemID in self.sMkt.serviceMarketRecentlyUsedModules["pyfaMarketRecentlyUsedModules"]: self.recentlyUsedModules.add(self.sMkt.getItem(itemID)) - wx.PostEvent(self.mainFrame, ItemSelected(itemID=self.active[sel].ID)) + wx.PostEvent(self.mainFrame, ItemSelected(itemID=self.active[sel].ID)) def storeRecentlyUsedMarketItem(self, itemID): if len(self.sMkt.serviceMarketRecentlyUsedModules["pyfaMarketRecentlyUsedModules"]) > MAX_RECENTLY_USED_MODULES: @@ -361,6 +371,7 @@ class ItemView(Display): btn.setMetaAvailable(False) def scheduleSearch(self, event=None): + self.searchTimer.Stop() # Cancel any pending timers search = self.marketBrowser.search.GetLineText(0) # Make sure we do not count wildcard as search symbol realsearch = search.replace("*", "") diff --git a/gui/notesView.py b/gui/notesView.py index d4f8703cc..5c44a26c2 100644 --- a/gui/notesView.py +++ b/gui/notesView.py @@ -24,8 +24,14 @@ class NotesView(wx.Panel): sFit = Fit.getInstance() fit = sFit.getFit(event.fitID) + self.saveTimer.Stop() # cancel any pending timers + self.Parent.Parent.DisablePage(self, not fit or fit.isStructure) + # when switching fits, ensure that we save the notes for the previous fit + if self.lastFitId is not None: + sFit.editNotes(self.lastFitId, self.editNotes.GetValue()) + if event.fitID is None and self.lastFitId is not None: self.lastFitId = None event.Skip() @@ -41,7 +47,4 @@ class NotesView(wx.Panel): def delayedSave(self, event): sFit = Fit.getInstance() - fit = sFit.getFit(self.lastFitId) - newNotes = self.editNotes.GetValue() - fit.notes = newNotes - wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fit.ID)) + sFit.editNotes(self.lastFitId, self.editNotes.GetValue()) diff --git a/gui/shipBrowser.py b/gui/shipBrowser.py index 33561de5b..75a1548ce 100644 --- a/gui/shipBrowser.py +++ b/gui/shipBrowser.py @@ -337,15 +337,21 @@ class NavigationPanel(SFItem.SFBrowserItem): self.newBmpH = BitmapLoader.getBitmap("fit_add_small", "gui") self.resetBmpH = BitmapLoader.getBitmap("freset_small", "gui") self.switchBmpH = BitmapLoader.getBitmap("fit_switch_view_mode_small", "gui") + self.recentBmpH = BitmapLoader.getBitmap("frecent_small", "gui") switchImg = BitmapLoader.getImage("fit_switch_view_mode_small", "gui") switchImg = switchImg.AdjustChannels(1, 1, 1, 0.4) self.switchBmpD = wx.BitmapFromImage(switchImg) + recentImg = BitmapLoader.getImage("frecent_small", "gui") + recentImg = recentImg.AdjustChannels(1, 1, 1, 0.4) + self.recentBmpD = wx.BitmapFromImage(recentImg) + self.resetBmp = self.AdjustChannels(self.resetBmpH) self.rewBmp = self.AdjustChannels(self.rewBmpH) self.searchBmp = self.AdjustChannels(self.searchBmpH) self.switchBmp = self.AdjustChannels(self.switchBmpH) + self.recentBmp = self.AdjustChannels(self.recentBmpH) self.newBmp = self.AdjustChannels(self.newBmpH) self.toolbar.AddButton(self.resetBmp, "Ship groups", clickCallback=self.OnHistoryReset, @@ -356,6 +362,9 @@ class NavigationPanel(SFItem.SFBrowserItem): self.btnSwitch = self.toolbar.AddButton(self.switchBmpD, "Hide empty ship groups", clickCallback=self.ToggleEmptyGroupsView, hoverBitmap=self.switchBmpH, show=False) + self.btnRecent = self.toolbar.AddButton(self.recentBmpD, "Recent Fits", + clickCallback=self.ToggleRecentShips, hoverBitmap=self.recentBmpH, + show=True) modifier = "CTRL" if 'wxMac' not in wx.PlatformInfo else "CMD" self.toolbar.AddButton(self.searchBmp, "Search fittings ({}+F)".format(modifier), clickCallback=self.ToggleSearchBox, @@ -415,6 +424,27 @@ class NavigationPanel(SFItem.SFBrowserItem): def OnResize(self, event): self.Refresh() + def ToggleRecentShips(self, bool=None, emitEvent=True): + # this is so janky. Need to revaluate pretty much entire ship browser. >.< + toggle = bool if bool is not None else not self.shipBrowser.recentFits + + if not toggle: + self.shipBrowser.recentFits = False + self.btnRecent.label = "Recent Fits" + self.btnRecent.normalBmp = self.recentBmpD + + if emitEvent: + wx.PostEvent(self.shipBrowser, Stage1Selected()) + else: + self.shipBrowser.recentFits = True + self.btnRecent.label = "Hide Recent Fits" + self.btnRecent.normalBmp = self.recentBmp + + if emitEvent: + sFit = Fit.getInstance() + fits = sFit.getRecentFits() + wx.PostEvent(self.shipBrowser, ImportSelected(fits=fits, back=True, recent=True)) + def ToggleEmptyGroupsView(self): if self.shipBrowser.filterShipsWithNoFits: self.shipBrowser.filterShipsWithNoFits = False @@ -453,11 +483,13 @@ class NavigationPanel(SFItem.SFBrowserItem): wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID)) def OnHistoryReset(self): + self.ToggleRecentShips(False, False) if self.shipBrowser.browseHist: self.shipBrowser.browseHist = [] self.gotoStage(1, 0) def OnHistoryBack(self): + self.ToggleRecentShips(False, False) if len(self.shipBrowser.browseHist) > 0: stage, data = self.shipBrowser.browseHist.pop() self.gotoStage(stage, data) @@ -537,6 +569,7 @@ class NavigationPanel(SFItem.SFBrowserItem): self.bkBitmap.mFactor = mFactor def gotoStage(self, stage, data=None): + self.shipBrowser.recentFits = False if stage == 1: wx.PostEvent(self.Parent, Stage1Selected()) elif stage == 2: @@ -572,6 +605,7 @@ class ShipBrowser(wx.Panel): self._stage3ShipName = "" self.fitIDMustEditName = -1 self.filterShipsWithNoFits = False + self.recentFits = False self.racesFilter = {} @@ -628,7 +662,8 @@ class ShipBrowser(wx.Panel): def RefreshList(self, event): stage = self.GetActiveStage() - if stage == 3 or stage == 4: + + if stage in (3, 4, 5): self.lpane.RefreshList(True) event.Skip() @@ -671,6 +706,7 @@ class ShipBrowser(wx.Panel): return self.racesFilter[race] def stage1(self, event): + self.navpanel.ToggleRecentShips(False, False) self._lastStage = self._activeStage self._activeStage = 1 self.lastdata = 0 @@ -726,6 +762,7 @@ class ShipBrowser(wx.Panel): def stage2Callback(self, data): if self.GetActiveStage() != 2: return + self.navpanel.ToggleRecentShips(False, False) categoryID = self._stage2Data ships = list(data[1]) @@ -811,7 +848,7 @@ class ShipBrowser(wx.Panel): return info[1] def stage3(self, event): - + self.navpanel.ToggleRecentShips(False, False) self.lpane.ShowLoading(False) # If back is False, do not append to history. This could be us calling @@ -862,8 +899,8 @@ class ShipBrowser(wx.Panel): shipTrait = ship.traits.traitText if (ship.traits is not None) else "" # empty string if no traits - for ID, name, booster, timestamp in fitList: - self.lpane.AddWidget(FitItem(self.lpane, ID, (shipName, shipTrait, name, booster, timestamp), shipID)) + for ID, name, booster, timestamp, notes in fitList: + self.lpane.AddWidget(FitItem(self.lpane, ID, (shipName, shipTrait, name, booster, timestamp, notes), shipID)) self.lpane.RefreshList() self.lpane.Thaw() @@ -903,11 +940,11 @@ class ShipBrowser(wx.Panel): ShipItem(self.lpane, ship.ID, (ship.name, shipTrait, len(sFit.getFitsWithShip(ship.ID))), ship.race)) - for ID, name, shipID, shipName, booster, timestamp in fitList: + for ID, name, shipID, shipName, booster, timestamp, notes in fitList: ship = sMkt.getItem(shipID) shipTrait = ship.traits.traitText if (ship.traits is not None) else "" # empty string if no traits - self.lpane.AddWidget(FitItem(self.lpane, ID, (shipName, shipTrait, name, booster, timestamp), shipID)) + self.lpane.AddWidget(FitItem(self.lpane, ID, (shipName, shipTrait, name, booster, timestamp, notes), shipID)) if len(ships) == 0 and len(fitList) == 0: self.lpane.AddWidget(PFStaticText(self.lpane, label=u"No matching results.")) self.lpane.RefreshList(doFocus=False) @@ -920,6 +957,10 @@ class ShipBrowser(wx.Panel): self.Layout() def importStage(self, event): + """ + The import stage handles both displaying fits after importing as well as displaying recent fits. todo: need to + reconcile these two better into a more uniform function, right now hacked together to get working + """ self.lpane.ShowLoading(False) self.navpanel.ShowNewFitButton(False) @@ -933,29 +974,27 @@ class ShipBrowser(wx.Panel): fits = event.fits - # sort by ship name, then fit name - fits.sort(key=lambda _fit: (_fit.ship.item.name, _fit.name)) - self.lastdata = fits self.lpane.Freeze() self.lpane.RemoveAllChildren() if fits: for fit in fits: - shipTrait = fit.ship.item.traits.traitText if (fit.ship.item.traits is not None) else "" - # empty string if no traits + shipItem = fit[3] + shipTrait = shipItem.traits.traitText if (shipItem.traits is not None) else "" self.lpane.AddWidget(FitItem( self.lpane, - fit.ID, + fit[0], ( - fit.ship.item.name, + shipItem.name, shipTrait, - fit.name, - fit.booster, - fit.timestamp, + fit[1], + False, + fit[2], + fit[4] ), - fit.ship.item.ID, + shipItem.ID, )) self.lpane.RefreshList(doFocus=False) self.lpane.Thaw() @@ -1156,7 +1195,9 @@ class ShipItem(SFItem.SFBrowserItem): self.raceDropShadowBmp = drawUtils.CreateDropShadowBitmap(self.raceBmp, 0.2) - self.SetToolTip(wx.ToolTip(self.shipTrait)) + sFit = Fit.getInstance() + if self.shipTrait and sFit.serviceFittingOptions["showShipBrowserTooltip"]: + self.SetToolTip(wx.ToolTip(self.shipTrait)) self.shipBrowser = self.Parent.Parent @@ -1435,7 +1476,7 @@ class PFBitmapFrame(wx.Frame): class FitItem(SFItem.SFBrowserItem): - def __init__(self, parent, fitID=None, shipFittingInfo=("Test", "TestTrait", "cnc's avatar", 0, 0), shipID=None, + def __init__(self, parent, fitID=None, shipFittingInfo=("Test", "TestTrait", "cnc's avatar", 0, 0, None), shipID=None, itemData=None, id=wx.ID_ANY, pos=wx.DefaultPosition, size=(0, 40), style=0): @@ -1470,7 +1511,8 @@ class FitItem(SFItem.SFBrowserItem): self.shipBmp = BitmapLoader.getBitmap("ship_no_image_big", "gui") self.shipFittingInfo = shipFittingInfo - self.shipName, self.shipTrait, self.fitName, self.fitBooster, self.timestamp = shipFittingInfo + self.shipName, self.shipTrait, self.fitName, self.fitBooster, self.timestamp, self.notes = shipFittingInfo + self.shipTrait = re.sub("<.*?>", " ", self.shipTrait) # see GH issue #62 @@ -1492,8 +1534,9 @@ class FitItem(SFItem.SFBrowserItem): self.dragTLFBmp = None self.bkBitmap = None - if self.shipTrait != "": # show no tooltip if no trait available - self.SetToolTip(wx.ToolTip(u'{}\n{}\n{}'.format(self.shipName, u'─' * 20, self.shipTrait))) + + self.__setToolTip() + self.padding = 4 self.editWidth = 150 @@ -1549,14 +1592,27 @@ class FitItem(SFItem.SFBrowserItem): # self.animCount = 0 # ===================================================================== + """ + # Remove this bit as the time stuff is non-functional (works... but not exactly sure what it's meant to do) self.selTimerID = wx.NewId() self.selTimer = wx.Timer(self, self.selTimerID) self.selTimer.Start(100) + """ self.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu) self.Bind(wx.EVT_MIDDLE_UP, self.OpenNewTab) + def __setToolTip(self): + sFit = Fit.getInstance() + # show no tooltip if no trait available or setting is disabled + if self.shipTrait and sFit.serviceFittingOptions["showShipBrowserTooltip"]: + notes = "" + if self.notes: + notes = u'─' * 20 + u"\nNotes: {}\n".format( + self.notes[:197] + '...' if len(self.notes) > 200 else self.notes) + self.SetToolTip(wx.ToolTip(u'{}\n{}{}\n{}'.format(self.shipName, notes, u'─' * 20, self.shipTrait))) + def OpenNewTab(self, evt): self.selectFit(newTab=True) @@ -1641,6 +1697,7 @@ class FitItem(SFItem.SFBrowserItem): def OnTimer(self, event): + # @todo: figure out what exactly this is supposed to accomplish if self.selTimerID == event.GetId(): ctimestamp = time.time() interval = 5 @@ -1722,7 +1779,6 @@ class FitItem(SFItem.SFBrowserItem): self.fitName = fitName sFit.renameFit(self.fitID, self.fitName) wx.PostEvent(self.mainFrame, FitRenamed(fitID=self.fitID)) - self.Refresh() else: self.tcFitName.SetValue(self.fitName) @@ -1746,6 +1802,7 @@ class FitItem(SFItem.SFBrowserItem): self.deleteFit() def deleteFit(self, event=None): + pyfalog.debug("Deleting ship fit.") if self.deleted: return else: @@ -1888,8 +1945,8 @@ class FitItem(SFItem.SFBrowserItem): mdc.SetFont(self.fontNormal) - fitDate = time.localtime(self.timestamp) - fitLocalDate = "%d/%02d/%02d %02d:%02d" % (fitDate[0], fitDate[1], fitDate[2], fitDate[3], fitDate[4]) + fitDate = self.timestamp.strftime("%m/%d/%Y %H:%M") + fitLocalDate = fitDate # "%d/%02d/%02d %02d:%02d" % (fitDate[0], fitDate[1], fitDate[2], fitDate[3], fitDate[4]) pfdate = drawUtils.GetPartialText(mdc, fitLocalDate, self.toolbarx - self.textStartx - self.padding * 2 - self.thoverw) @@ -1942,6 +1999,17 @@ class FitItem(SFItem.SFBrowserItem): state = SFItem.SB_ITEM_NORMAL return state + def Refresh(self): + activeFit = self.mainFrame.getActiveFit() + if activeFit == self.fitID: + sFit = Fit.getInstance() + fit = sFit.getFit(activeFit) + self.timestamp = fit.modifiedCoalesce + self.notes = fit.notes + self.__setToolTip() + + SFItem.SFBrowserItem.Refresh(self) + def RenderBackground(self): rect = self.GetRect() diff --git a/gui/statsView.py b/gui/statsView.py index 464359695..dc4c0276b 100644 --- a/gui/statsView.py +++ b/gui/statsView.py @@ -52,6 +52,7 @@ from gui.builtinStatsViews import ( # noqa: E402, F401 rechargeViewFull, targetingMiscViewMinimal, priceViewFull, + priceViewMinimal, outgoingViewFull, outgoingViewMinimal, ) diff --git a/gui/utils/floatspin.py b/gui/utils/floatspin.py new file mode 100644 index 000000000..b2e21f178 --- /dev/null +++ b/gui/utils/floatspin.py @@ -0,0 +1,1662 @@ +# --------------------------------------------------------------------------- # +# FLOATSPIN Control wxPython IMPLEMENTATION +# Python Code By: +# +# Andrea Gavana, @ 16 Nov 2005 +# Latest Revision: 03 Jan 2014, 23.00 GMT +# +# +# TODO List/Caveats +# +# 1. Ay Idea? +# +# For All Kind Of Problems, Requests Of Enhancements And Bug Reports, Please +# Write To Me At: +# +# andrea.gavana@gmail.com +# andrea.gavana@maerskoil.com +# +# Or, Obviously, To The wxPython Mailing List!!! +# +# +# End Of Comments +# --------------------------------------------------------------------------- # + + +""" +:class:`FloatSpin` implements a floating point :class:`SpinCtrl`. + + +Description +=========== + +:class:`FloatSpin` implements a floating point :class:`SpinCtrl`. It is built using a custom +:class:`PyControl`, composed by a :class:`TextCtrl` and a :class:`SpinButton`. In order to +correctly handle floating points numbers without rounding errors or non-exact +floating point representations, :class:`FloatSpin` uses the great :class:`FixedPoint` class +from Tim Peters. + +What you can do: + +- Set the number of representative digits for your floating point numbers; +- Set the floating point format (``%f``, ``%F``, ``%e``, ``%E``, ``%g``, ``%G``); +- Set the increment of every ``EVT_FLOATSPIN`` event; +- Set minimum, maximum values for :class:`FloatSpin` as well as its range; +- Change font and colour for the underline :class:`TextCtrl`. + + +Usage +===== + +Usage example:: + + import wx + import wx.lib.agw.floatspin as FS + + class MyFrame(wx.Frame): + + def __init__(self, parent): + + wx.Frame.__init__(self, parent, -1, "FloatSpin Demo") + + panel = wx.Panel(self) + + floatspin = FS.FloatSpin(panel, -1, pos=(50, 50), min_val=0, max_val=1, + increment=0.01, value=0.1, agwStyle=FS.FS_LEFT) + floatspin.SetFormat("%f") + floatspin.SetDigits(2) + + + # our normal wxApp-derived class, as usual + + app = wx.App(0) + + frame = MyFrame(None) + app.SetTopWindow(frame) + frame.Show() + + app.MainLoop() + + + +Events +====== + +:class:`FloatSpin` catches 3 different types of events: + +1) Spin events: events generated by spinning up/down the spinbutton; +2) Char events: playing with up/down arrows of the keyboard increase/decrease + the value of :class:`FloatSpin`; +3) Mouse wheel event: using the wheel will change the value of :class:`FloatSpin`. + +In addition, there are some other functionalities: + +- It remembers the initial value as a default value, call meth:~FloatSpin.SetToDefaultValue`, or + press ``Esc`` to return to it; +- ``Shift`` + arrow = 2 * increment (or ``Shift`` + mouse wheel); +- ``Ctrl`` + arrow = 10 * increment (or ``Ctrl`` + mouse wheel); +- ``Alt`` + arrow = 100 * increment (or ``Alt`` + mouse wheel); +- Combinations of ``Shift``, ``Ctrl``, ``Alt`` increment the :class:`FloatSpin` value by the + product of the factors; +- ``PgUp`` & ``PgDn`` = 10 * increment * the product of the ``Shift``, ``Ctrl``, ``Alt`` + factors; +- ``Space`` sets the control's value to it's last valid state. + + +Window Styles +============= + +This class supports the following window styles: + +=============== =========== ================================================== +Window Styles Hex Value Description +=============== =========== ================================================== +``FS_READONLY`` 0x1 Sets :class:`FloatSpin` as read-only control. +``FS_LEFT`` 0x2 Horizontally align the underlying :class:`TextCtrl` on the left. +``FS_CENTRE`` 0x4 Horizontally align the underlying :class:`TextCtrl` on center. +``FS_RIGHT`` 0x8 Horizontally align the underlying :class:`TextCtrl` on the right. +=============== =========== ================================================== + + +Events Processing +================= + +This class processes the following events: + +================= ================================================== +Event Name Description +================= ================================================== +``EVT_FLOATSPIN`` Emitted when the user changes the value of :class:`FloatSpin`, either with the mouse or with the keyboard. +================= ================================================== + + +License And Version +=================== + +:class:`FloatSpin` control is distributed under the wxPython license. + +Latest revision: Andrea Gavana @ 03 Jan 2014, 23.00 GMT + +Version 0.9 + + +Backward Incompatibilities +========================== + +Modifications to allow `min_val` or `max_val` to be ``None`` done by: + +James Bigler, +SCI Institute, University of Utah, +March 14, 2007 + +:note: Note that the changes I made will break backward compatibility, + because I changed the contructor's parameters from `min` / `max` to + `min_val` / `max_val` to be consistent with the other functions and to + eliminate any potential confusion with the built in `min` and `max` + functions. + +You specify open ranges like this (you can equally do this in the +constructor):: + + SetRange(min_val=1, max_val=None) # [1, ] + SetRange(min_val=None, max_val=0) # [ , 0] + +or no range:: + + SetRange(min_val=None, max_val=None) # [ , ] + +""" + +# ---------------------------------------------------------------------- +# Beginning Of FLOATSPIN wxPython Code +# ---------------------------------------------------------------------- + +import wx +import locale +from math import ceil, floor + +# Set The Styles For The Underline wx.TextCtrl +FS_READONLY = 1 +""" Sets :class:`FloatSpin` as read-only control. """ +FS_LEFT = 2 +""" Horizontally align the underlying :class:`TextCtrl` on the left. """ +FS_CENTRE = 4 +""" Horizontally align the underlying :class:`TextCtrl` on center. """ +FS_RIGHT = 8 +""" Horizontally align the underlying :class:`TextCtrl` on the right. """ + +# Define The FloatSpin Event +wxEVT_FLOATSPIN = wx.NewEventType() + +# -----------------------------------# +# FloatSpinEvent +# -----------------------------------# + +EVT_FLOATSPIN = wx.PyEventBinder(wxEVT_FLOATSPIN, 1) +""" Emitted when the user changes the value of :class:`FloatSpin`, either with the mouse or""" \ +""" with the keyboard. """ + + +# ---------------------------------------------------------------------------- # +# Class FloatSpinEvent +# ---------------------------------------------------------------------------- # + +class FloatSpinEvent(wx.PyCommandEvent): + """ This event will be sent when a ``EVT_FLOATSPIN`` event is mapped in the parent. """ + + def __init__(self, eventType, eventId=1, nSel=-1, nOldSel=-1): + """ + Default class constructor. + + :param `eventType`: the event type; + :param `eventId`: the event identifier; + :param `nSel`: the current selection; + :param `nOldSel`: the old selection. + """ + + wx.PyCommandEvent.__init__(self, eventType, eventId) + self._eventType = eventType + + def SetPosition(self, pos): + """ + Sets event position. + + :param `pos`: an integer specyfing the event position. + """ + + self._position = pos + + def GetPosition(self): + """ Returns event position. """ + + return self._position + + +# ---------------------------------------------------------------------------- +# FloatTextCtrl +# ---------------------------------------------------------------------------- + + +class FloatTextCtrl(wx.TextCtrl): + """ + A class which holds a :class:`TextCtrl`, one of the two building blocks + of :class:`FloatSpin`. + """ + + def __init__(self, parent, id=wx.ID_ANY, value="", pos=wx.DefaultPosition, + size=wx.DefaultSize, style=wx.TE_NOHIDESEL | wx.TE_PROCESS_ENTER, + validator=wx.DefaultValidator, + name=wx.TextCtrlNameStr): + """ + Default class constructor. + Used internally. Do not call directly this class in your code! + + :param `parent`: the :class:`FloatTextCtrl` parent; + :param `id`: an identifier for the control: a value of -1 is taken to mean a default; + :param `value`: default text value; + :param `pos`: the control position. A value of (-1, -1) indicates a default position, + chosen by either the windowing system or wxPython, depending on platform; + :param `size`: the control size. A value of (-1, -1) indicates a default size, + chosen by either the windowing system or wxPython, depending on platform; + :param `style`: the window style; + :param `validator`: the window validator; + :param `name`: the window name. + + """ + + wx.TextCtrl.__init__(self, parent, id, value, pos, size, style, validator, name) + + self._parent = parent + self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy) + self.Bind(wx.EVT_CHAR, self.OnChar) + self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + + def OnDestroy(self, event): + """ + Handles the ``wx.EVT_WINDOW_DESTROY`` event for :class:`FloatTextCtrl`. + + :param `event`: a :class:`WindowDestroyEvent` event to be processed. + + :note: This method tries to correctly handle the control destruction under MSW. + """ + + if self._parent: + self._parent._textctrl = None + self._parent = None + + def OnChar(self, event): + """ + Handles the ``wx.EVT_CHAR`` event for :class:`FloatTextCtrl`. + + :param `event`: a :class:`KeyEvent` event to be processed. + """ + + if self._parent: + self._parent.OnChar(event) + + def OnKillFocus(self, event): + """ + Handles the ``wx.EVT_KILL_FOCUS`` event for :class:`FloatTextCtrl`. + + :param `event`: a :class:`FocusEvent` event to be processed. + + :note: This method synchronizes the :class:`SpinButton` and the :class:`TextCtrl` + when focus is lost. + """ + + if self._parent: + self._parent.SyncSpinToText(True) + + event.Skip() + + +# ---------------------------------------------------------------------------- # +# FloatSpin +# This Is The Main Class Implementation +# ---------------------------------------------------------------------------- # + +class FloatSpin(wx.PyControl): + """ + :class:`FloatSpin` implements a floating point :class:`SpinCtrl`. It is built using a custom + :class:`PyControl`, composed by a :class:`TextCtrl` and a :class:`SpinButton`. In order to + correctly handle floating points numbers without rounding errors or non-exact + floating point representations, :class:`FloatSpin` uses the great :class:`FixedPoint` class + from Tim Peters. + """ + + def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, + size=(95, -1), style=0, value=0.0, min_val=None, max_val=None, + increment=1.0, digits=-1, agwStyle=FS_LEFT, + name="FloatSpin"): + """ + Default class constructor. + + :param `parent`: the :class:`FloatSpin` parent; + :param `id`: an identifier for the control: a value of -1 is taken to mean a default; + :param `pos`: the control position. A value of (-1, -1) indicates a default position, + chosen by either the windowing system or wxPython, depending on platform; + :param `size`: the control size. A value of (-1, -1) indicates a default size, + chosen by either the windowing system or wxPython, depending on platform; + :param `style`: the window style; + :param `value`: is the current value for :class:`FloatSpin`; + :param `min_val`: the minimum value, ignored if ``None``; + :param `max_val`: the maximum value, ignored if ``None``; + :param `increment`: the increment for every :class:`FloatSpinEvent` event; + :param `digits`: number of representative digits for your floating point numbers; + :param `agwStyle`: one of the following bits: + + =============== =========== ================================================== + Window Styles Hex Value Description + =============== =========== ================================================== + ``FS_READONLY`` 0x1 Sets :class:`FloatSpin` as read-only control. + ``FS_LEFT`` 0x2 Horizontally align the underlying :class:`TextCtrl` on the left. + ``FS_CENTRE`` 0x4 Horizontally align the underlying :class:`TextCtrl` on center. + ``FS_RIGHT`` 0x8 Horizontally align the underlying :class:`TextCtrl` on the right. + =============== =========== ================================================== + + :param `name`: the window name. + + """ + + wx.PyControl.__init__(self, parent, id, pos, size, style | wx.NO_BORDER | + wx.NO_FULL_REPAINT_ON_RESIZE | wx.CLIP_CHILDREN, + wx.DefaultValidator, name) + + # Don't call SetRange here, because it will try to modify + # self._value whose value doesn't exist yet. + self.SetRangeDontClampValue(min_val, max_val) + self._value = self.ClampValue(FixedPoint(str(value), 20)) + self._defaultvalue = self._value + self._increment = FixedPoint(str(increment), 20) + self._spinmodifier = FixedPoint(str(1.0), 20) + self._digits = digits + self._snapticks = False + self._spinbutton = None + self._textctrl = None + self._spinctrl_bestsize = wx.Size(-999, -999) + + # start Philip Semanchuk addition + # The textbox & spin button are drawn slightly differently + # depending on the platform. The difference is most pronounced + # under OS X. + if "__WXMAC__" in wx.PlatformInfo: + self._gap = 8 + self._spin_top = 3 + self._text_left = 4 + self._text_top = 4 + elif "__WXMSW__" in wx.PlatformInfo: + self._gap = 1 + self._spin_top = 0 + self._text_left = 0 + self._text_top = 0 + else: + # GTK + self._gap = -1 + self._spin_top = 0 + self._text_left = 0 + self._text_top = 0 + # end Philip Semanchuk addition + + self.SetLabel(name) + self.SetForegroundColour(parent.GetForegroundColour()) + + width = size[0] + height = size[1] + best_size = self.DoGetBestSize() + + if width == -1: + width = best_size.GetWidth() + if height == -1: + height = best_size.GetHeight() + + self._validkeycode = [43, 44, 45, 46, 69, 101, 127, 314] + self._validkeycode.extend(range(48, 58)) + self._validkeycode.extend([wx.WXK_RETURN, wx.WXK_TAB, wx.WXK_BACK, + wx.WXK_LEFT, wx.WXK_RIGHT]) + + self._spinbutton = wx.SpinButton(self, wx.ID_ANY, wx.DefaultPosition, + size=(-1, height), + style=wx.SP_ARROW_KEYS | wx.SP_VERTICAL | + wx.SP_WRAP) + + txtstyle = wx.TE_NOHIDESEL | wx.TE_PROCESS_ENTER + + if agwStyle & FS_RIGHT: + txtstyle = txtstyle | wx.TE_RIGHT + elif agwStyle & FS_CENTRE: + txtstyle = txtstyle | wx.TE_CENTER + + if agwStyle & FS_READONLY: + txtstyle = txtstyle | wx.TE_READONLY + + self._textctrl = FloatTextCtrl(self, wx.ID_ANY, str(self._value), + wx.DefaultPosition, + (width - self._spinbutton.GetSize().GetWidth(), height), + txtstyle) + + # start Philip Semanchuk addition + # Setting the textctrl's size in the ctor also sets its min size. + # But the textctrl is entirely controlled by the parent floatspin + # control and should accept whatever size its parent dictates, so + # here we tell it to forget its min size. + self._textctrl.SetMinSize(wx.DefaultSize) + # Setting the spin buttons's size in the ctor also sets its min size. + # Under OS X that results in a rendering artifact because spin buttons + # are a little shorter than textboxes. + # Setting the min size to the default allows OS X to draw the spin + # button correctly. However, Windows and KDE take the call to + # SetMinSize() as a cue to size the spin button taller than the + # textbox, so we avoid the call there. + if "__WXMAC__" in wx.PlatformInfo: + self._spinbutton.SetMinSize(wx.DefaultSize) + # end Philip Semanchuk addition + + self._mainsizer = wx.BoxSizer(wx.HORIZONTAL) + # Ensure the spin button is shown, and the text widget takes + # all remaining free space + self._mainsizer.Add(self._textctrl, 1) + self._mainsizer.Add(self._spinbutton, 0) + self.SetSizer(self._mainsizer) + self._mainsizer.Layout() + + self.SetFormat() + self.SetDigits(digits) + + # set the value here without generating an event + + decimal = locale.localeconv()["decimal_point"] + strs = ("%100." + str(self._digits) + self._textformat[1]) % self._value + strs = strs.replace(".", decimal) + + strs = strs.strip() + strs = self.ReplaceDoubleZero(strs) + + self._textctrl.SetValue(strs) + + if not (agwStyle & FS_READONLY): + self.Bind(wx.EVT_SPIN_UP, self.OnSpinUp) + self.Bind(wx.EVT_SPIN_DOWN, self.OnSpinDown) + self._spinbutton.Bind(wx.EVT_LEFT_DOWN, self.OnSpinMouseDown) + + self._textctrl.Bind(wx.EVT_TEXT_ENTER, self.OnTextEnter) + self._textctrl.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel) + self._spinbutton.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel) + + self.Bind(wx.EVT_SET_FOCUS, self.OnFocus) + self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) + self.Bind(wx.EVT_SIZE, self.OnSize) + + # start Philip Semanchuk move + self.SetBestSize((width, height)) + # end Philip Semanchuk move + + def OnDestroy(self, event): + """ + Handles the ``wx.EVT_WINDOW_DESTROY`` event for :class:`FloatSpin`. + + :param `event`: a :class:`WindowDestroyEvent` event to be processed. + + :note: This method tries to correctly handle the control destruction under MSW. + """ + + # Null This Since MSW Sends KILL_FOCUS On Deletion + if self._textctrl: + self._textctrl._parent = None + self._textctrl.Destroy() + self._textctrl = None + + self._spinbutton.Destroy() + self._spinbutton = None + + def DoGetBestSize(self): + """ + Gets the size which best suits the window: for a control, it would be the + minimal size which doesn't truncate the control, for a panel - the same + size as it would have after a call to `Fit()`. + + :note: Overridden from :class:`PyControl`. + """ + + if self._spinctrl_bestsize.x == -999: + + spin = wx.SpinCtrl(self, -1) + self._spinctrl_bestsize = spin.GetBestSize() + + # oops something went wrong, set to reasonable value + if self._spinctrl_bestsize.GetWidth() < 20: + self._spinctrl_bestsize.SetWidth(95) + if self._spinctrl_bestsize.GetHeight() < 10: + self._spinctrl_bestsize.SetHeight(22) + + spin.Destroy() + + return self._spinctrl_bestsize + + def DoSendEvent(self): + """ Send the event to the parent. """ + + event = wx.CommandEvent(wx.wxEVT_COMMAND_SPINCTRL_UPDATED, self.GetId()) + event.SetEventObject(self) + event.SetInt(int(self._value + 0.5)) + + if self._textctrl: + event.SetString(self._textctrl.GetValue()) + + self.GetEventHandler().ProcessEvent(event) + + eventOut = FloatSpinEvent(wxEVT_FLOATSPIN, self.GetId()) + eventOut.SetPosition(int(self._value + 0.5)) + eventOut.SetEventObject(self) + self.GetEventHandler().ProcessEvent(eventOut) + + def OnSpinMouseDown(self, event): + """ + Handles the ``wx.EVT_LEFT_DOWN`` event for :class:`FloatSpin`. + + :param `event`: a :class:`MouseEvent` event to be processed. + + :note: This method works on the underlying :class:`SpinButton`. + """ + + modifier = FixedPoint(str(1.0), 20) + if event.ShiftDown(): + modifier = modifier * 2.0 + if event.ControlDown(): + modifier = modifier * 10.0 + if event.AltDown(): + modifier = modifier * 100.0 + + self._spinmodifier = modifier + + event.Skip() + + def OnSpinUp(self, event): + """ + Handles the ``wx.EVT_SPIN_UP`` event for :class:`FloatSpin`. + + :param `event`: a :class:`SpinEvent` event to be processed. + """ + + if self._textctrl and self._textctrl.IsModified(): + self.SyncSpinToText(False) + + if self.InRange(self._value + self._increment * self._spinmodifier): + self._value = self._value + self._increment * self._spinmodifier + self.SetValue(self._value) + self.DoSendEvent() + + def OnSpinDown(self, event): + """ + Handles the ``wx.EVT_SPIN_DOWN`` event for :class:`FloatSpin`. + + :param `event`: a :class:`SpinEvent` event to be processed. + """ + + if self._textctrl and self._textctrl.IsModified(): + self.SyncSpinToText(False) + + if self.InRange(self._value - self._increment * self._spinmodifier): + self._value = self._value - self._increment * self._spinmodifier + self.SetValue(self._value) + self.DoSendEvent() + + def OnTextEnter(self, event): + """ + Handles the ``wx.EVT_TEXT_ENTER`` event for :class:`FloatSpin`. + + :param `event`: a :class:`KeyEvent` event to be processed. + + :note: This method works on the underlying :class:`TextCtrl`. + """ + + self.SyncSpinToText(True) + event.Skip() + + def OnChar(self, event): + """ + Handles the ``wx.EVT_CHAR`` event for :class:`FloatSpin`. + + :param `event`: a :class:`KeyEvent` event to be processed. + + :note: This method works on the underlying :class:`TextCtrl`. + """ + + modifier = FixedPoint(str(1.0), 20) + if event.ShiftDown(): + modifier = modifier * 2.0 + if event.ControlDown(): + modifier = modifier * 10.0 + if event.AltDown(): + modifier = modifier * 100.0 + + keycode = event.GetKeyCode() + + if keycode == wx.WXK_UP: + + if self._textctrl and self._textctrl.IsModified(): + self.SyncSpinToText(False) + + self.SetValue(self._value + self._increment * modifier) + self.DoSendEvent() + + elif keycode == wx.WXK_DOWN: + + if self._textctrl and self._textctrl.IsModified(): + self.SyncSpinToText(False) + + self.SetValue(self._value - self._increment * modifier) + self.DoSendEvent() + + elif keycode == wx.WXK_PRIOR: + + if self._textctrl and self._textctrl.IsModified(): + self.SyncSpinToText(False) + + self.SetValue(self._value + 10.0 * self._increment * modifier) + self.DoSendEvent() + + elif keycode == wx.WXK_NEXT: + + if self._textctrl and self._textctrl.IsModified(): + self.SyncSpinToText(False) + + self.SetValue(self._value - 10.0 * self._increment * modifier) + self.DoSendEvent() + + elif keycode == wx.WXK_SPACE: + + self.SetValue(self._value) + event.Skip(False) + + elif keycode == wx.WXK_ESCAPE: + + self.SetToDefaultValue() + self.DoSendEvent() + + elif keycode == wx.WXK_TAB: + + new_event = wx.NavigationKeyEvent() + new_event.SetEventObject(self.GetParent()) + new_event.SetDirection(not event.ShiftDown()) + # CTRL-TAB changes the (parent) window, i.e. switch notebook page + new_event.SetWindowChange(event.ControlDown()) + new_event.SetCurrentFocus(self) + self.GetParent().GetEventHandler().ProcessEvent(new_event) + + else: + if keycode not in self._validkeycode: + return + + event.Skip() + + def OnMouseWheel(self, event): + """ + Handles the ``wx.EVT_MOUSEWHEEL`` event for :class:`FloatSpin`. + + :param `event`: a :class:`MouseEvent` event to be processed. + """ + + modifier = FixedPoint(str(1.0), 20) + if event.ShiftDown(): + modifier = modifier * 2.0 + if event.ControlDown(): + modifier = modifier * 10.0 + if event.AltDown(): + modifier = modifier * 100.0 + + if self._textctrl and self._textctrl.IsModified(): + self.SyncSpinToText(False) + + if event.GetWheelRotation() > 0: + self.SetValue(self._value + self._increment * modifier) + self.DoSendEvent() + + else: + + self.SetValue(self._value - self._increment * modifier) + self.DoSendEvent() + + def OnSize(self, event): + """ + Handles the ``wx.EVT_SIZE`` event for :class:`FloatSpin`. + + :param `event`: a :class:`SizeEvent` event to be processed. + + :note: This method resizes the text control and reposition the spin button when + resized. + """ + # start Philip Semanchuk addition + event_width = event.GetSize().width + + self._textctrl.SetPosition((self._text_left, self._text_top)) + + text_width, text_height = self._textctrl.GetSizeTuple() + + spin_width, _ = self._spinbutton.GetSizeTuple() + + text_width = event_width - (spin_width + self._gap + self._text_left) + + self._textctrl.SetSize(wx.Size(text_width, event.GetSize().height)) + + # The spin button is always snug against the right edge of the + # control. + self._spinbutton.SetPosition((event_width - spin_width, self._spin_top)) + + event.Skip() + # end Philip Semanchuk addition + + def ReplaceDoubleZero(self, strs): + """ + Replaces the (somewhat) python ugly `+e000` with `+e00`. + + :param `strs`: a string (possibly) containing a `+e00` substring. + """ + + if self._textformat not in ["%g", "%e", "%E", "%G"]: + return strs + + if strs.find("e+00") >= 0: + strs = strs.replace("e+00", "e+0") + elif strs.find("e-00") >= 0: + strs = strs.replace("e-00", "e-0") + elif strs.find("E+00") >= 0: + strs = strs.replace("E+00", "E+0") + elif strs.find("E-00") >= 0: + strs = strs.replace("E-00", "E-0") + + return strs + + def SetValue(self, value): + """ + Sets the :class:`FloatSpin` value. + + :param `value`: the new value. + """ + if not self._textctrl or not self.InRange(value): + return + + if self._snapticks and self._increment != 0.0: + + finite, snap_value = self.IsFinite(value) + + if not finite: # FIXME What To Do About A Failure? + + if (snap_value - floor(snap_value) < ceil(snap_value) - snap_value): + value = self._defaultvalue + floor(snap_value) * self._increment + else: + value = self._defaultvalue + ceil(snap_value) * self._increment + + decimal = locale.localeconv()["decimal_point"] + strs = ("%100." + str(self._digits) + self._textformat[1]) % value + strs = strs.replace(".", decimal) + strs = strs.strip() + strs = self.ReplaceDoubleZero(strs) + + if value != self._value or strs != self._textctrl.GetValue(): + self._textctrl.SetValue(strs) + self._textctrl.DiscardEdits() + self._value = value + + def GetValue(self): + """ Returns the :class:`FloatSpin` value. """ + + return float(self._value) + + def SetRangeDontClampValue(self, min_val, max_val): + """ + Sets the allowed range. + + :param `min_val`: the minimum value for :class:`FloatSpin`. If it is ``None`` it is + ignored; + :param `max_val`: the maximum value for :class:`FloatSpin`. If it is ``None`` it is + ignored. + + :note: This method doesn't modify the current value. + """ + + if (min_val != None): + self._min = FixedPoint(str(min_val), 20) + else: + self._min = None + if (max_val != None): + self._max = FixedPoint(str(max_val), 20) + else: + self._max = None + + def SetRange(self, min_val, max_val): + """ + Sets the allowed range. + + :param `min_val`: the minimum value for :class:`FloatSpin`. If it is ``None`` it is + ignored; + :param `max_val`: the maximum value for :class:`FloatSpin`. If it is ``None`` it is + ignored. + + :note: This method doesn't modify the current value. + + :note: You specify open ranges like this (you can equally do this in the + constructor):: + + SetRange(min_val=1, max_val=None) + SetRange(min_val=None, max_val=0) + + + or no range:: + + SetRange(min_val=None, max_val=None) + + """ + + self.SetRangeDontClampValue(min_val, max_val) + + value = self.ClampValue(self._value) + if (value != self._value): + self.SetValue(value) + + def ClampValue(self, var): + """ + Clamps `var` between `_min` and `_max` depending if the range has + been specified. + + :param `var`: the value to be clamped. + + :return: A clamped copy of `var`. + """ + + if (self._min != None): + if (var < self._min): + var = self._min + return var + + if (self._max != None): + if (var > self._max): + var = self._max + + return var + + def SetIncrement(self, increment): + """ + Sets the increment for every ``EVT_FLOATSPIN`` event. + + :param `increment`: a floating point number specifying the :class:`FloatSpin` increment. + """ + + if increment < 1. / 10.0 ** self._digits: + raise Exception("\nERROR: Increment Should Be Greater Or Equal To 1/(10**digits).") + + self._increment = FixedPoint(str(increment), 20) + self.SetValue(self._value) + + def GetIncrement(self): + """ Returns the increment for every ``EVT_FLOATSPIN`` event. """ + + return self._increment + + def SetDigits(self, digits=-1): + """ + Sets the number of digits to show. + + :param `digits`: the number of digits to show. If `digits` < 0, :class:`FloatSpin` + tries to calculate the best number of digits based on input values passed + in the constructor. + """ + + if digits < 0: + incr = str(self._increment) + if incr.find(".") < 0: + digits = 0 + else: + digits = len(incr[incr.find(".") + 1:]) + + self._digits = digits + + self.SetValue(self._value) + + def GetDigits(self): + """ Returns the number of digits shown. """ + + return self._digits + + def SetFormat(self, fmt="%f"): + """ + Set the string format to use. + + :param `fmt`: the new string format to use. One of the following strings: + + ====== ================================= + Format Description + ====== ================================= + 'e' Floating point exponential format (lowercase) + 'E' Floating point exponential format (uppercase) + 'f' Floating point decimal format + 'F' Floating point decimal format + 'g' Floating point format. Uses lowercase exponential format if exponent is less than -4 or not less than precision, decimal format otherwise + 'G' Floating point format. Uses uppercase exponential format if exponent is less than -4 or not less than precision, decimal format otherwise + ====== ================================= + + """ + + if fmt not in ["%f", "%g", "%e", "%E", "%F", "%G"]: + raise Exception('\nERROR: Bad Float Number Format: ' + repr(fmt) + '. It Should Be ' \ + 'One Of "%f", "%g", "%e", "%E", "%F", "%G"') + + self._textformat = fmt + + if self._digits < 0: + self.SetDigits() + + self.SetValue(self._value) + + def GetFormat(self): + """ + Returns the string format in use. + + :see: :meth:`~FloatSpin.SetFormat` for a list of valid string formats. + """ + + return self._textformat + + def SetDefaultValue(self, defaultvalue): + """ + Sets the :class:`FloatSpin` default value. + + :param `defaultvalue`: a floating point value representing the new default + value for :class:`FloatSpin`. + """ + + if self.InRange(defaultvalue): + self._defaultvalue = FixedPoint(str(defaultvalue), 20) + + def GetDefaultValue(self): + """ Returns the :class:`FloatSpin` default value. """ + + return self._defaultvalue + + def IsDefaultValue(self): + """ Returns whether the current value is the default value or not. """ + + return self._value == self._defaultvalue + + def SetToDefaultValue(self): + """ Sets :class:`FloatSpin` value to its default value. """ + + self.SetValue(self._defaultvalue) + + def SetSnapToTicks(self, forceticks=True): + """ + Force the value to always be divisible by the increment. Initially ``False``. + + :param `forceticks`: ``True`` to force the snap to ticks option, ``False`` otherwise. + + :note: This uses the default value as the basis, you will get strange results + for very large differences between the current value and default value + when the increment is very small. + """ + + if self._snapticks != forceticks: + self._snapticks = forceticks + self.SetValue(self._value) + + def GetSnapToTicks(self): + """ Returns whether the snap to ticks option is active or not. """ + + return self._snapticks + + def OnFocus(self, event): + """ + Handles the ``wx.EVT_SET_FOCUS`` event for :class:`FloatSpin`. + + :param `event`: a :class:`FocusEvent` event to be processed. + """ + + if self._textctrl: + self._textctrl.SetFocus() + + event.Skip() + + def OnKillFocus(self, event): + """ + Handles the ``wx.EVT_KILL_FOCUS`` event for :class:`FloatSpin`. + + :param `event`: a :class:`FocusEvent` event to be processed. + """ + + self.SyncSpinToText(True) + event.Skip() + + def SyncSpinToText(self, send_event=True, force_valid=True): + """ + Synchronize the underlying :class:`TextCtrl` with :class:`SpinButton`. + + :param `send_event`: ``True`` to send a ``EVT_FLOATSPIN`` event, ``False`` + otherwise; + :param `force_valid`: ``True`` to force a valid value (i.e. inside the + provided range), ``False`` otherwise. + """ + + if not self._textctrl: + return + + curr = self._textctrl.GetValue() + curr = curr.strip() + decimal = locale.localeconv()["decimal_point"] + curr = curr.replace(decimal, ".") + + if curr: + try: + curro = float(curr) + curr = FixedPoint(curr, 20) + except: + self.SetValue(self._value) + return + + if force_valid or not self.HasRange() or self.InRange(curr): + + if force_valid and self.HasRange(): + curr = self.ClampValue(curr) + + if self._value != curr: + self.SetValue(curr) + + if send_event: + self.DoSendEvent() + + elif force_valid: + + # textctrl is out of sync, discard and reset + self.SetValue(self.GetValue()) + + def SetFont(self, font=None): + """ + Sets the underlying :class:`TextCtrl` font. + + :param `font`: a valid instance of :class:`Font`. + """ + + if font is None: + font = wx.SystemSettings_GetFont(wx.SYS_DEFAULT_GUI_FONT) + + if not self._textctrl: + return False + + return self._textctrl.SetFont(font) + + def GetFont(self): + """ Returns the underlying :class:`TextCtrl` font. """ + + if not self._textctrl: + return self.GetFont() + + return self._textctrl.GetFont() + + def GetMin(self): + """ + Returns the minimum value for :class:`FloatSpin`. It can be a + number or ``None`` if no minimum is present. + """ + + return self._min + + def GetMax(self): + """ + Returns the maximum value for :class:`FloatSpin`. It can be a + number or ``None`` if no minimum is present. + """ + + return self._max + + def HasRange(self): + """ Returns whether :class:`FloatSpin` range has been set or not. """ + + return (self._min != None) or (self._max != None) + + def InRange(self, value): + """ + Returns whether a value is inside :class:`FloatSpin` range. + + :param `value`: the value to test. + """ + + if (not self.HasRange()): + return True + if (self._min != None): + if (value < self._min): + return False + if (self._max != None): + if (value > self._max): + return False + return True + + def GetTextCtrl(self): + """ Returns the underlying :class:`TextCtrl`. """ + + return self._textctrl + + def IsFinite(self, value): + """ + Tries to determine if a value is finite or infinite/NaN. + + :param `value`: the value to test. + """ + + try: + snap_value = (value - self._defaultvalue) / self._increment + finite = True + except: + finite = False + snap_value = None + + return finite, snap_value + + +# Class FixedPoint, version 0.0.4. +# Released to the public domain 28-Mar-2001, +# by Tim Peters (tim.one@home.com). + +# Provided as-is; use at your own risk; no warranty; no promises; enjoy! + + +# 28-Mar-01 ver 0.0,4 +# Use repr() instead of str() inside __str__, because str(long) changed +# since this was first written (used to produce trailing "L", doesn't +# now). +# +# 09-May-99 ver 0,0,3 +# Repaired __sub__(FixedPoint, string); was blowing up. +# Much more careful conversion of float (now best possible). +# Implemented exact % and divmod. +# +# 14-Oct-98 ver 0,0,2 +# Added int, long, frac. Beefed up docs. Removed DECIMAL_POINT +# and MINUS_SIGN globals to discourage bloating this class instead +# of writing formatting wrapper classes (or subclasses) +# +# 11-Oct-98 ver 0,0,1 +# posted to c.l.py + +__version__ = 0, 0, 4 + +# The default value for the number of decimal digits carried after the +# decimal point. This only has effect at compile-time. +DEFAULT_PRECISION = 2 +""" The default value for the number of decimal digits carried after the decimal point. This only has effect at compile-time. """ + + +class FixedPoint(object): + """ + FixedPoint objects support decimal arithmetic with a fixed number of + digits (called the object's precision) after the decimal point. The + number of digits before the decimal point is variable & unbounded. + + The precision is user-settable on a per-object basis when a FixedPoint + is constructed, and may vary across FixedPoint objects. The precision + may also be changed after construction via `FixedPoint.set_precision(p)`. + Note that if the precision of a FixedPoint is reduced via :meth:`FixedPoint.set_precision() `, + information may be lost to rounding. + + Example:: + + >>> x = FixedPoint("5.55") # precision defaults to 2 + >>> print x + 5.55 + >>> x.set_precision(1) # round to one fraction digit + >>> print x + 5.6 + >>> print FixedPoint("5.55", 1) # same thing setting to 1 in constructor + 5.6 + >>> repr(x) # returns constructor string that reproduces object exactly + "FixedPoint('5.6', 1)" + >>> + + + When :class:`FixedPoint` objects of different precision are combined via + - * /, + the result is computed to the larger of the inputs' precisions, which also + becomes the precision of the resulting :class:`FixedPoint` object. Example:: + + >>> print FixedPoint("3.42") + FixedPoint("100.005", 3) + 103.425 + >>> + + + When a :class:`FixedPoint` is combined with other numeric types (ints, floats, + strings representing a number) via + - * /, then similarly the computation + is carried out using -- and the result inherits -- the :class:`FixedPoint`'s + precision. Example:: + + >>> print FixedPoint(1) / 7 + 0.14 + >>> print FixedPoint(1, 30) / 7 + 0.142857142857142857142857142857 + >>> + + + The string produced by `str(x)` (implictly invoked by `print`) always + contains at least one digit before the decimal point, followed by a + decimal point, followed by exactly `x.get_precision()` digits. If `x` is + negative, `str(x)[0] == "-"`. + + The :class:`FixedPoint` constructor can be passed an int, long, string, float, + :class:`FixedPoint`, or any object convertible to a float via `float()` or to a + long via `long()`. Passing a precision is optional; if specified, the + precision must be a non-negative int. There is no inherent limit on + the size of the precision, but if very very large you'll probably run + out of memory. + + Note that conversion of floats to :class:`FixedPoint` can be surprising, and + should be avoided whenever possible. Conversion from string is exact + (up to final rounding to the requested precision), so is greatly + preferred. Example:: + + >>> print FixedPoint(1.1e30) + 1099999999999999993725589651456.00 + >>> print FixedPoint("1.1e30") + 1100000000000000000000000000000.00 + >>> + + + """ + + # the exact value is self.n / 10**self.p; + # self.n is a long; self.p is an int + + def __init__(self, value=0, precision=DEFAULT_PRECISION): + """ + Default class constructor. + + :param `value`: the initial value; + :param `precision`: must be an int >= 0, and defaults to ``DEFAULT_PRECISION``. + """ + + self.n = self.p = 0 + self.set_precision(precision) + p = self.p + + if isinstance(value, type("42.3e5")): + n, exp = _string2exact(value) + # exact value is n*10**exp = n*10**(exp+p)/10**p + effective_exp = exp + p + if effective_exp > 0: + n = n * _tento(effective_exp) + elif effective_exp < 0: + n = _roundquotient(n, _tento(-effective_exp)) + self.n = n + return + + if isinstance(value, type(42)) or isinstance(value, type(42L)): + self.n = long(value) * _tento(p) + return + + if isinstance(value, FixedPoint): + temp = value.copy() + temp.set_precision(p) + self.n, self.p = temp.n, temp.p + return + + if isinstance(value, type(42.0)): + # XXX ignoring infinities and NaNs and overflows for now + import math + f, e = math.frexp(abs(value)) + assert f == 0 or 0.5 <= f < 1.0 + # |value| = f * 2**e exactly + + # Suck up CHUNK bits at a time; 28 is enough so that we suck + # up all bits in 2 iterations for all known binary double- + # precision formats, and small enough to fit in an int. + CHUNK = 28 + top = 0L + # invariant: |value| = (top + f) * 2**e exactly + while f: + f = math.ldexp(f, CHUNK) + digit = int(f) + assert digit >> CHUNK == 0 + top = (top << CHUNK) | digit + f = f - digit + assert 0.0 <= f < 1.0 + e = e - CHUNK + + # now |value| = top * 2**e exactly + # want n such that n / 10**p = top * 2**e, or + # n = top * 10**p * 2**e + top = top * _tento(p) + if e >= 0: + n = top << e + else: + n = _roundquotient(top, 1L << -e) + if value < 0: + n = -n + self.n = n + return + + if isinstance(value, type(42 - 42j)): + raise TypeError("can't convert complex to FixedPoint: " + + `value`) + + # can we coerce to a float? + yes = 1 + try: + asfloat = float(value) + except: + yes = 0 + if yes: + self.__init__(asfloat, p) + return + + # similarly for long + yes = 1 + try: + aslong = long(value) + except: + yes = 0 + if yes: + self.__init__(aslong, p) + return + + raise TypeError("can't convert to FixedPoint: " + `value`) + + def get_precision(self): + """ + Return the precision of this :class:`FixedPoint`. + + :note: The precision is the number of decimal digits carried after + the decimal point, and is an int >= 0. + """ + + return self.p + + def set_precision(self, precision=DEFAULT_PRECISION): + """ + Change the precision carried by this :class:`FixedPoint` to `precision`. + + :param `precision`: must be an int >= 0, and defaults to + ``DEFAULT_PRECISION``. + + :note: If `precision` is less than this :class:`FixedPoint`'s current precision, + information may be lost to rounding. + """ + + try: + p = int(precision) + except: + raise TypeError("precision not convertable to int: " + + `precision`) + if p < 0: + raise ValueError("precision must be >= 0: " + `precision`) + + if p > self.p: + self.n = self.n * _tento(p - self.p) + elif p < self.p: + self.n = _roundquotient(self.n, _tento(self.p - p)) + self.p = p + + def __str__(self): + + n, p = self.n, self.p + i, f = divmod(abs(n), _tento(p)) + if p: + frac = repr(f)[:-1] + frac = "0" * (p - len(frac)) + frac + else: + frac = "" + return "-"[:n < 0] + \ + repr(i)[:-1] + \ + "." + frac + + def __repr__(self): + + return "FixedPoint" + `(str(self), self.p)` + + def copy(self): + """ Create a copy of the current :class:`FixedPoint`. """ + + return _mkFP(self.n, self.p) + + __copy__ = __deepcopy__ = copy + + def __cmp__(self, other): + + if (other == None): + return 1 + xn, yn, p = _norm(self, other) + return cmp(xn, yn) + + def __hash__(self): + # caution! == values must have equal hashes, and a FixedPoint + # is essentially a rational in unnormalized form. There's + # really no choice here but to normalize it, so hash is + # potentially expensive. + n, p = self.__reduce() + + # Obscurity: if the value is an exact integer, p will be 0 now, + # so the hash expression reduces to hash(n). So FixedPoints + # that happen to be exact integers hash to the same things as + # their int or long equivalents. This is Good. But if a + # FixedPoint happens to have a value exactly representable as + # a float, their hashes may differ. This is a teensy bit Bad. + return hash(n) ^ hash(p) + + def __nonzero__(self): + return self.n != 0 + + def __neg__(self): + return _mkFP(-self.n, self.p) + + def __abs__(self): + if self.n >= 0: + return self.copy() + else: + return -self + + def __add__(self, other): + n1, n2, p = _norm(self, other) + # n1/10**p + n2/10**p = (n1+n2)/10**p + return _mkFP(n1 + n2, p) + + __radd__ = __add__ + + def __sub__(self, other): + if not isinstance(other, FixedPoint): + other = FixedPoint(other, self.p) + return self.__add__(-other) + + def __rsub__(self, other): + return (-self) + other + + def __mul__(self, other): + n1, n2, p = _norm(self, other) + # n1/10**p * n2/10**p = (n1*n2/10**p)/10**p + return _mkFP(_roundquotient(n1 * n2, _tento(p)), p) + + __rmul__ = __mul__ + + def __div__(self, other): + n1, n2, p = _norm(self, other) + if n2 == 0: + raise ZeroDivisionError("FixedPoint division") + if n2 < 0: + n1, n2 = -n1, -n2 + # n1/10**p / (n2/10**p) = n1/n2 = (n1*10**p/n2)/10**p + return _mkFP(_roundquotient(n1 * _tento(p), n2), p) + + def __rdiv__(self, other): + n1, n2, p = _norm(self, other) + return _mkFP(n2, p) / self + + def __divmod__(self, other): + n1, n2, p = _norm(self, other) + if n2 == 0: + raise ZeroDivisionError("FixedPoint modulo") + # floor((n1/10**p)/(n2*10**p)) = floor(n1/n2) + q = n1 / n2 + # n1/10**p - q * n2/10**p = (n1 - q * n2)/10**p + return q, _mkFP(n1 - q * n2, p) + + def __rdivmod__(self, other): + n1, n2, p = _norm(self, other) + return divmod(_mkFP(n2, p), self) + + def __mod__(self, other): + return self.__divmod__(other)[1] + + def __rmod__(self, other): + n1, n2, p = _norm(self, other) + return _mkFP(n2, p).__mod__(self) + + # caution! float can lose precision + def __float__(self): + n, p = self.__reduce() + return float(n) / float(_tento(p)) + + # XXX should this round instead? + # XXX note e.g. long(-1.9) == -1L and long(1.9) == 1L in Python + # XXX note that __int__ inherits whatever __long__ does, + # XXX and .frac() is affected too + def __long__(self): + answer = abs(self.n) / _tento(self.p) + if self.n < 0: + answer = -answer + return answer + + def __int__(self): + return int(self.__long__()) + + def frac(self): + """ + Returns fractional portion as a :class:`FixedPoint`. + + :note: In :class:`FixedPoint`, + + this equality holds true:: + + x = x.frac() + long(x) + + + """ + return self - long(self) + + # return n, p s.t. self == n/10**p and n % 10 != 0 + def __reduce(self): + n, p = self.n, self.p + if n == 0: + p = 0 + while p and n % 10 == 0: + p = p - 1 + n = n / 10 + return n, p + + +# return 10L**n + +def _tento(n, cache={}): + try: + return cache[n] + except KeyError: + answer = cache[n] = 10L ** n + return answer + + +# return xn, yn, p s.t. +# p = max(x.p, y.p) +# x = xn / 10**p +# y = yn / 10**p +# +# x must be FixedPoint to begin with; if y is not FixedPoint, +# it inherits its precision from x. +# +# Note that this is called a lot, so default-arg tricks are helpful. + +def _norm(x, y, isinstance=isinstance, FixedPoint=FixedPoint, + _tento=_tento): + assert isinstance(x, FixedPoint) + if not isinstance(y, FixedPoint): + y = FixedPoint(y, x.p) + xn, yn = x.n, y.n + xp, yp = x.p, y.p + if xp > yp: + yn = yn * _tento(xp - yp) + p = xp + elif xp < yp: + xn = xn * _tento(yp - xp) + p = yp + else: + p = xp # same as yp + return xn, yn, p + + +def _mkFP(n, p, FixedPoint=FixedPoint): + f = FixedPoint() + f.n = n + f.p = p + return f + + +# divide x by y, rounding to int via nearest-even +# y must be > 0 +# XXX which rounding modes are useful? + +def _roundquotient(x, y): + assert y > 0 + n, leftover = divmod(x, y) + c = cmp(leftover << 1, y) + # c < 0 <-> leftover < y/2, etc + if c > 0 or (c == 0 and (n & 1) == 1): + n = n + 1 + return n + + +# crud for parsing strings +import re + +# There's an optional sign at the start, and an optional exponent +# at the end. The exponent has an optional sign and at least one +# digit. In between, must have either at least one digit followed +# by an optional fraction, or a decimal point followed by at least +# one digit. Yuck. + +_parser = re.compile(r""" + \s* + (?P[-+])? + ( + (?P\d+) (\. (?P\d*))? + | + \. (?P\d+) + ) + ([eE](?P[-+]? \d+))? + \s* $ +""", re.VERBOSE).match + +del re + + +# return n, p s.t. float string value == n * 10**p exactly + +def _string2exact(s): + m = _parser(s) + if m is None: + raise ValueError("can't parse as number: " + `s`) + + exp = m.group('exp') + if exp is None: + exp = 0 + else: + exp = int(exp) + + intpart = m.group('int') + if intpart is None: + intpart = "0" + fracpart = m.group('onlyfrac') + else: + fracpart = m.group('frac') + if fracpart is None or fracpart == "": + fracpart = "0" + assert intpart + assert fracpart + + i, f = long(intpart), long(fracpart) + nfrac = len(fracpart) + i = i * _tento(nfrac) + f + exp = exp - nfrac + + if m.group('sign') == "-": + i = -i + + return i, exp diff --git a/gui/utils/helpers_wxPython.py b/gui/utils/helpers_wxPython.py new file mode 100644 index 000000000..79f8ed206 --- /dev/null +++ b/gui/utils/helpers_wxPython.py @@ -0,0 +1,8 @@ +import wx + + +def YesNoDialog(question=u'Are you sure you want to do this?', caption=u'Yes or no?'): + dlg = wx.MessageDialog(None, question, caption, wx.YES_NO | wx.ICON_QUESTION) + result = dlg.ShowModal() == wx.ID_YES + dlg.Destroy() + return result diff --git a/imgs/gui/frecent_small.png b/imgs/gui/frecent_small.png new file mode 100644 index 000000000..911da3f1d Binary files /dev/null and b/imgs/gui/frecent_small.png differ diff --git a/imgs/renders/42241.png b/imgs/renders/42241.png new file mode 100644 index 000000000..14dc0ee57 Binary files /dev/null and b/imgs/renders/42241.png differ diff --git a/imgs/renders/42242.png b/imgs/renders/42242.png new file mode 100644 index 000000000..3610d4afe Binary files /dev/null and b/imgs/renders/42242.png differ diff --git a/imgs/renders/42243.png b/imgs/renders/42243.png new file mode 100644 index 000000000..14e131173 Binary files /dev/null and b/imgs/renders/42243.png differ diff --git a/imgs/renders/44993.png b/imgs/renders/44993.png new file mode 100644 index 000000000..d1c2b0b74 Binary files /dev/null and b/imgs/renders/44993.png differ diff --git a/imgs/renders/44995.png b/imgs/renders/44995.png new file mode 100644 index 000000000..c75e4ca35 Binary files /dev/null and b/imgs/renders/44995.png differ diff --git a/pyfa.py b/pyfa.py index 06f8632d1..4faa1eddf 100755 --- a/pyfa.py +++ b/pyfa.py @@ -18,17 +18,29 @@ # along with pyfa. If not, see . # ============================================================================== -import sys +import inspect import os -import os.path +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 optparse import OptionParser, BadOptionError, AmbiguousOptionError +try: + import wxversion +except ImportError: + wxversion = None + +try: + import sqlalchemy +except ImportError: + sqlalchemy = None -import logbook -from logbook import TimedRotatingFileHandler, Logger, StreamHandler, NestedSetup, FingersCrossedHandler, NullHandler, \ - CRITICAL, ERROR, WARNING, DEBUG, INFO pyfalog = Logger(__name__) @@ -38,6 +50,7 @@ class PassThroughOptionParser(OptionParser): OSX passes -psn_0_* argument, which is something that pyfa does not handle. See GH issue #423 """ + def _process_args(self, largs, rargs, values): while rargs: try: @@ -47,7 +60,7 @@ class PassThroughOptionParser(OptionParser): largs.append(e.opt_str) -class LoggerWriter: +class LoggerWriter(object): def __init__(self, level): # self.level is really like using log.debug(message) # at least in my case @@ -67,6 +80,88 @@ class LoggerWriter: self.level(sys.stderr) +class PreCheckException(Exception): + def __init__(self, msg): + try: + ln = sys.exc_info()[-1].tb_lineno + except AttributeError: + ln = inspect.currentframe().f_back.f_lineno + self.message = "{0.__name__} (line {1}): {2}".format(type(self), ln, msg) + self.args = self.message, + + +def handleGUIException(exc_type, exc_value, exc_traceback): + try: + # Try and import wx in case it's missing. + # noinspection PyPackageRequirements + import wx + from gui.errorDialog import ErrorFrame + except: + # noinspection PyShadowingNames + wx = None + # noinspection PyShadowingNames + ErrorFrame = None + + tb = traceback.format_tb(exc_traceback) + + try: + + # Try and output to our log handler + with logging_setup.threadbound(): + module_list = list(set(sys.modules) & set(globals())) + if module_list: + pyfalog.info("Imported Python Modules:") + for imported_module in module_list: + module_details = sys.modules[imported_module] + pyfalog.info("{0}: {1}", imported_module, getattr(module_details, '__version__', '')) + + pyfalog.critical("Exception in main thread: {0}", exc_value.message) + # Print the base level traceback + traceback.print_tb(exc_traceback) + + if wx and ErrorFrame: + pyfa_gui = wx.App(False) + if exc_type == PreCheckException: + msgbox = wx.MessageBox(exc_value.message, 'Error', wx.ICON_ERROR | wx.STAY_ON_TOP) + msgbox.ShowModal() + else: + ErrorFrame(exc_value, tb) + + pyfa_gui.MainLoop() + + pyfalog.info("Exiting.") + except: + # Most likely logging isn't available. Try and output to the console + module_list = list(set(sys.modules) & set(globals())) + if module_list: + pyfalog.info("Imported Python Modules:") + for imported_module in module_list: + module_details = sys.modules[imported_module] + print(str(imported_module) + ": " + str(getattr(module_details, '__version__', ''))) + + print("Exception in main thread: " + str(exc_value.message)) + traceback.print_tb(exc_traceback) + + if wx and ErrorFrame: + pyfa_gui = wx.App(False) + if exc_type == PreCheckException: + msgbox = wx.MessageBox(exc_value.message, 'Error', wx.ICON_ERROR | wx.STAY_ON_TOP) + msgbox.ShowModal() + else: + ErrorFrame(exc_value, tb) + + pyfa_gui.MainLoop() + + print("Exiting.") + + finally: + # TODO: Add cleanup when exiting here. + sys.exit() + + +# Replace the uncaught exception handler with our own handler. +sys.excepthook = handleGUIException + # Parse command line options usage = "usage: %prog [--root]" parser = PassThroughOptionParser(usage=usage) @@ -92,64 +187,6 @@ elif options.logginglevel == "Debug": else: options.logginglevel = ERROR -if not hasattr(sys, 'frozen'): - - if sys.version_info < (2, 6) or sys.version_info > (3, 0): - print("Pyfa requires python 2.x branch ( >= 2.6 )\nExiting.") - sys.exit(1) - - try: - import wxversion - except ImportError: - wxversion = None - print("Cannot find wxPython\nYou can download wxPython (2.8+) from http://www.wxpython.org/") - sys.exit(1) - - try: - if options.force28 is True: - wxversion.select('2.8') - else: - wxversion.select(['3.0', '2.8']) - except wxversion.VersionError: - print("Installed wxPython version doesn't meet requirements.\nYou can download wxPython 2.8 or 3.0 from http://www.wxpython.org/") - sys.exit(1) - - try: - import sqlalchemy - - saVersion = sqlalchemy.__version__ - saMatch = re.match("([0-9]+).([0-9]+)([b\.])([0-9]+)", saVersion) - if saMatch: - saMajor = int(saMatch.group(1)) - saMinor = int(saMatch.group(2)) - betaFlag = True if saMatch.group(3) == "b" else False - saBuild = int(saMatch.group(4)) if not betaFlag else 0 - if saMajor == 0 and (saMinor < 5 or (saMinor == 5 and saBuild < 8)): - print("Pyfa requires sqlalchemy 0.5.8 at least but current sqlalchemy version is %s\n" - "You can download sqlalchemy (0.5.8+) from http://www.sqlalchemy.org/".format(sqlalchemy.__version__)) - sys.exit(1) - else: - print("Unknown sqlalchemy version string format, skipping check") - - except ImportError: - sqlalchemy = None - print("Cannot find sqlalchemy.\nYou can download sqlalchemy (0.6+) from http://www.sqlalchemy.org/") - sys.exit(1) - - # check also for dateutil module installed. - try: - # noinspection PyPackageRequirements - import dateutil.parser # noqa - Copied import statement from service/update.py - except ImportError: - dateutil = None - print("Cannot find python-dateutil.\nYou can download python-dateutil from https://pypi.python.org/pypi/python-dateutil") - sys.exit(1) - - logVersion = logbook.__version__.split('.') - if int(logVersion[0]) < 1: - print ("Logbook version >= 1.0.0 is recommended. You may have some performance issues by continuing to use an earlier version.") - - if __name__ == "__main__": # Configure paths if options.rootsavedata is True: @@ -161,90 +198,177 @@ if __name__ == "__main__": config.debug = options.debug - # Import everything - # noinspection PyPackageRequirements - import wx - import os - import os.path + # convert to unicode if it is set + if options.savepath is not None: + options.savepath = unicode(options.savepath) + config.defPaths(options.savepath) + + # Basic logging initialization + + # Logging levels: + ''' + logbook.CRITICAL + logbook.ERROR + logbook.WARNING + logbook.INFO + logbook.DEBUG + logbook.NOTSET + ''' + + if options.debug: + savePath_filename = "Pyfa_debug.log" + else: + savePath_filename = "Pyfa.log" + + config.logPath = os.path.join(config.savePath, savePath_filename) try: - # convert to unicode if it is set - if options.savepath is not None: - options.savepath = unicode(options.savepath) - config.defPaths(options.savepath) - - # Basic logging initialization - - # Logging levels: - ''' - logbook.CRITICAL - logbook.ERROR - logbook.WARNING - logbook.INFO - logbook.DEBUG - logbook.NOTSET - ''' - if options.debug: - savePath_filename = "Pyfa_debug.log" - else: - savePath_filename = "Pyfa.log" - - savePath_Destination = os.path.join(config.savePath, savePath_filename) - - try: - if options.debug: - logging_mode = "Debug" - logging_setup = NestedSetup([ - # make sure we never bubble up to the stderr handler - # if we run out of setup handling - NullHandler(), - StreamHandler( - sys.stdout, - bubble=False, - level=options.logginglevel - ), - TimedRotatingFileHandler( - savePath_Destination, - level=0, - backup_count=3, - bubble=True, - date_format='%Y-%m-%d', - ), - ]) - else: - logging_mode = "User" - logging_setup = NestedSetup([ - # make sure we never bubble up to the stderr handler - # if we run out of setup handling - NullHandler(), - FingersCrossedHandler( - TimedRotatingFileHandler( - savePath_Destination, - level=0, - backup_count=3, - bubble=False, - date_format='%Y-%m-%d', - ), - action_level=ERROR, - buffer_size=1000, - # pull_information=True, - # reset=False, - ) - ]) - except: - logging_mode = "Console Only" + logging_mode = "Debug" logging_setup = NestedSetup([ # make sure we never bubble up to the stderr handler # if we run out of setup handling NullHandler(), StreamHandler( - sys.stdout, - bubble=False + sys.stdout, + bubble=False, + level=options.logginglevel + ), + TimedRotatingFileHandler( + config.logPath, + level=0, + backup_count=3, + bubble=True, + date_format='%Y-%m-%d', + ), + ]) + else: + logging_mode = "User" + logging_setup = NestedSetup([ + # make sure we never bubble up to the stderr handler + # if we run out of setup handling + NullHandler(), + FingersCrossedHandler( + TimedRotatingFileHandler( + config.logPath, + level=0, + backup_count=3, + bubble=False, + date_format='%Y-%m-%d', + ), + action_level=ERROR, + buffer_size=1000, + # pull_information=True, + # reset=False, ) ]) + except: + print("Critical error attempting to setup logging. Falling back to console only.") + logging_mode = "Console Only" + logging_setup = NestedSetup([ + # make sure we never bubble up to the stderr handler + # if we run out of setup handling + NullHandler(), + StreamHandler( + sys.stdout, + bubble=False + ) + ]) + + with logging_setup.threadbound(): + pyfalog.info("Starting Pyfa") + + pyfalog.info("Logbook version: {0}", logbook_version) + + pyfalog.info("Running in logging mode: {0}", logging_mode) + pyfalog.info("Writing log file to: {0}", config.logPath) + + # Output all stdout (print) messages as warnings + try: + sys.stdout = LoggerWriter(pyfalog.warning) + except: + pyfalog.critical("Cannot redirect. Continuing without writing stdout to log.") + + # Output all stderr (stacktrace) messages as critical + try: + sys.stderr = LoggerWriter(pyfalog.critical) + except: + pyfalog.critical("Cannot redirect. Continuing without writing stderr to log.") + + pyfalog.info("OS version: {0}", platform.platform()) + + pyfalog.info("Python version: {0}", sys.version) + if sys.version_info < (2, 7) or sys.version_info > (3, 0): + exit_message = "Pyfa requires python 2.x branch ( >= 2.7 )." + raise PreCheckException(exit_message) + + if hasattr(sys, 'frozen'): + pyfalog.info("Running in a frozen state.") + else: + pyfalog.info("Running in a thawed state.") + + if not hasattr(sys, 'frozen') and wxversion: + try: + if options.force28 is True: + pyfalog.info("Selecting wx version: 2.8. (Forced)") + wxversion.select('2.8') + else: + pyfalog.info("Selecting wx versions: 3.0, 2.8") + wxversion.select(['3.0', '2.8']) + except: + pyfalog.warning("Unable to select wx version. Attempting to import wx without specifying the version.") + else: + if not wxversion: + pyfalog.warning("wxVersion not found. Attempting to import wx without specifying the version.") + + 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) + + pyfalog.info("wxPython version: {0}.", str(wx.VERSION_STRING)) + + if sqlalchemy is None: + exit_message = "\nCannot find sqlalchemy.\nYou can download sqlalchemy (0.6+) from http://www.sqlalchemy.org/" + raise PreCheckException(exit_message) + else: + saVersion = sqlalchemy.__version__ + saMatch = re.match("([0-9]+).([0-9]+)([b\.])([0-9]+)", saVersion) + config.saVersion = (int(saMatch.group(1)), int(saMatch.group(2)), int(saMatch.group(4))) + if saMatch: + saMajor = int(saMatch.group(1)) + saMinor = int(saMatch.group(2)) + betaFlag = True if saMatch.group(3) == "b" else False + saBuild = int(saMatch.group(4)) if not betaFlag else 0 + if saMajor == 0 and (saMinor < 5 or (saMinor == 5 and saBuild < 8)): + pyfalog.critical("Pyfa requires sqlalchemy 0.5.8 at least but current sqlAlchemy version is {0}", format(sqlalchemy.__version__)) + pyfalog.critical("You can download sqlAlchemy (0.5.8+) from http://www.sqlalchemy.org/") + pyfalog.critical("Attempting to run with unsupported version of sqlAlchemy.") + else: + pyfalog.info("Current version of sqlAlchemy is: {0}", sqlalchemy.__version__) + else: + pyfalog.warning("Unknown sqlalchemy version string format, skipping check. Version: {0}", sqlalchemy.__version__) + + logVersion = logbook_version.split('.') + if int(logVersion[0]) == 0 and int(logVersion[1]) < 10: + raise PreCheckException("Logbook version >= 0.10.0 is required.") + + if 'wxMac' not in wx.PlatformInfo or ('wxMac' in wx.PlatformInfo and wx.VERSION >= (3, 0)): + 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 @@ -254,41 +378,11 @@ if __name__ == "__main__": eos.db.saveddata_meta.create_all() - except Exception, e: - import traceback - from gui.errorDialog import ErrorFrame - - tb = traceback.format_exc() - - pyfa = wx.App(False) - ErrorFrame(e, tb) - pyfa.MainLoop() - sys.exit() - - with logging_setup.threadbound(): - # Don't redirect if frozen - if not hasattr(sys, 'frozen'): - # Output all stdout (print) messages as warnings - try: - sys.stdout = LoggerWriter(pyfalog.warning) - except ValueError, Exception: - pyfalog.critical("Cannot access log file. Continuing without writing stdout to log.") - - if not options.debug: - # Output all stderr (stacktrace) messages as critical - try: - sys.stderr = LoggerWriter(pyfalog.critical) - except ValueError, Exception: - pyfalog.critical("Cannot access log file. Continuing without writing stderr to log.") - - pyfalog.info("Starting Pyfa") - pyfalog.info("Running in logging mode: {0}", logging_mode) - - if hasattr(sys, 'frozen') and options.debug: - pyfalog.critical("Running in frozen mode with debug turned on. Forcing all output to be written to log.") - from gui.mainFrame import MainFrame pyfa = wx.App(False) MainFrame(options.title) pyfa.MainLoop() + + # TODO: Add some thread cleanup code here. Right now we bail, and that can lead to orphaned threads or threads not properly exiting. + sys.exit() diff --git a/requirements.txt b/requirements.txt index 7dc7d5651..42a072a9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ -logbook>=1.0.0 +logbook >= 0.10.0 matplotlib -PyYAML python-dateutil urllib3 requests==2.11.1 diff --git a/requirements_build_linux.txt b/requirements_build_linux.txt index 3a4640baf..28f42aa34 100644 --- a/requirements_build_linux.txt +++ b/requirements_build_linux.txt @@ -2,7 +2,7 @@ PyInstaller >= 3.2.1 cycler >= 0.10.0 functools32 >= 3.2.3 future >= 0.16.0 -numpy >= 1.12. +numpy >= 1.12 pyparsing >= 2.1.10 pytz >= 2016.10 six diff --git a/requirements_build_OSx.txt b/requirements_build_osx.txt similarity index 90% rename from requirements_build_OSx.txt rename to requirements_build_osx.txt index 51f6135fd..67b64e480 100644 --- a/requirements_build_OSx.txt +++ b/requirements_build_osx.txt @@ -2,7 +2,7 @@ PyInstaller >= 3.2.1 cycler >= 0.10.0 functools32 >= 3.2.3 future >= 0.16.0 -numpy >= 1.12. +numpy >= 1.12 pyparsing >= 2.1.10 pypiwin32 >= 219 pytz >= 2016.10 diff --git a/requirements_build_windows.txt b/requirements_build_windows.txt index 0d5cfb0ac..e18a0801d 100644 --- a/requirements_build_windows.txt +++ b/requirements_build_windows.txt @@ -3,7 +3,7 @@ cx_freeze == 4.3.4 cycler >= 0.10.0 functools32 >= 3.2.3 future >= 0.16.0 -numpy >= 1.12. +numpy >= 1.12 pyparsing >= 2.1.10 pypiwin32 >= 219 pytz >= 2016.10 diff --git a/requirements_test.txt b/requirements_test.txt index 33f8973cd..58e4ba7e9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,8 +1,10 @@ -pytest==3.0.3 -pytest-mock==1.2 -pytest-cov==2.3.1 -pytest-capturelog==0.7 -coverage==4.2 -coveralls==1.1 +pytest == 3.0.3 +pytest-mock == 1.2 +pytest-cov == 2.3.1 +pytest-capturelog == 0.7 +coverage == 4.2 +coveralls == 1.1 codecov +virtualenv>=15.0.0 tox +wheel diff --git a/scripts/jsonToSql.py b/scripts/jsonToSql.py index 173fc59f3..ad263df4b 100755 --- a/scripts/jsonToSql.py +++ b/scripts/jsonToSql.py @@ -49,9 +49,9 @@ def main(db, json_path): tables = { "clonegrades": eos.gamedata.AlphaCloneSkill, "dgmattribs": eos.gamedata.AttributeInfo, - "dgmeffects": eos.gamedata.EffectInfo, + "dgmeffects": eos.gamedata.Effect, "dgmtypeattribs": eos.gamedata.Attribute, - "dgmtypeeffects": eos.gamedata.Effect, + "dgmtypeeffects": eos.gamedata.ItemEffect, "dgmunits": eos.gamedata.Unit, "icons": eos.gamedata.Icon, "evecategories": eos.gamedata.Category, @@ -62,7 +62,6 @@ def main(db, json_path): "phbtraits": eos.gamedata.Traits, "phbmetadata": eos.gamedata.MetaData, "mapbulk_marketGroups": eos.gamedata.MarketGroup, - } fieldMapping = { diff --git a/service/character.py b/service/character.py index 1e5185b69..0ae8158cb 100644 --- a/service/character.py +++ b/service/character.py @@ -17,9 +17,11 @@ # along with pyfa. If not, see . # ============================================================================= +import sys import copy import itertools import json + from logbook import Logger import threading from codecs import open @@ -144,17 +146,20 @@ class Character(object): self.all5() def exportText(self): - data = "Pyfa exported plan for \"" + self.skillReqsDict['charname'] + "\"\n" - data += "=" * 79 + "\n" - data += "\n" - item = "" - for s in self.skillReqsDict['skills']: - if item == "" or not item == s["item"]: - item = s["item"] - data += "-" * 79 + "\n" - data += "Skills required for {}:\n".format(item) - data += "{}{}: {}\n".format(" " * s["indent"], s["skill"], int(s["level"])) - data += "-" * 79 + "\n" + data = u"Pyfa exported plan for \"" + self.skillReqsDict['charname'] + "\"\n" + data += u"=" * 79 + u"\n" + data += u"\n" + item = u"" + try: + for s in self.skillReqsDict['skills']: + if item == "" or not item == s["item"]: + item = s["item"] + data += u"-" * 79 + "\n" + data += u"Skills required for {}:\n".format(item) + data += u"{}{}: {}\n".format(" " * s["indent"], s["skill"], int(s["level"])) + data += u"-" * 79 + "\n" + except Exception: + pass return data @@ -272,11 +277,25 @@ class Character(object): skills.append((skill.ID, skill.name)) return skills + @staticmethod + def getSkillsByName(text): + items = eos.db.searchSkills(text) + skills = [] + for skill in items: + if skill.published is True: + skills.append((skill.ID, skill.name)) + return skills + @staticmethod def setAlphaClone(char, cloneID): char.alphaCloneID = cloneID eos.db.commit() + @staticmethod + def setSecStatus(char, secStatus): + char.secStatus = secStatus + eos.db.commit() + @staticmethod def getSkillDescription(itemID): return eos.db.getItem(itemID).description @@ -288,7 +307,7 @@ class Character(object): @staticmethod def getSkillLevel(charID, skillID): skill = eos.db.getCharacter(charID).getSkill(skillID) - return skill.level if skill.learned else "Not learned", skill.isDirty + return float(skill.level) if skill.learned else "Not learned", skill.isDirty @staticmethod def getDirtySkills(charID): @@ -350,26 +369,13 @@ class Character(object): char.chars = json.dumps(charList) return charList - @staticmethod - def apiFetch(charID, charName): - dbChar = eos.db.getCharacter(charID) - dbChar.defaultChar = charName + def apiFetch(self, charID, charName, callback): + thread = UpdateAPIThread(charID, charName, (self.apiFetchCallback, callback)) + thread.start() - api = EVEAPIConnection() - auth = api.auth(keyID=dbChar.apiID, vCode=dbChar.apiKey) - apiResult = auth.account.Characters() - charID = None - for char in apiResult.characters: - if char.name == charName: - charID = char.characterID - - if charID is None: - return - - sheet = auth.character(charID).CharacterSheet() - - dbChar.apiUpdateCharSheet(sheet.skills) + def apiFetchCallback(self, guiCallback, e=None): eos.db.commit() + wx.CallAfter(guiCallback, e) @staticmethod def apiUpdateCharSheet(charID, skills): @@ -378,16 +384,17 @@ class Character(object): eos.db.commit() @staticmethod - def changeLevel(charID, skillID, level, persist=False): + def changeLevel(charID, skillID, level, persist=False, ifHigher=False): char = eos.db.getCharacter(charID) skill = char.getSkill(skillID) - if isinstance(level, basestring) or level > 5 or level < 0: - skill.level = None - else: - skill.level = level - if persist: - skill.saveLevel() + if ifHigher and level < skill.level: + return + + if isinstance(level, basestring) or level > 5 or level < 0: + skill.setLevel(None, persist) + else: + skill.setLevel(level, persist) eos.db.commit() @@ -458,3 +465,38 @@ class Character(object): self._checkRequirements(fit, char, req, subs) return reqs + + +class UpdateAPIThread(threading.Thread): + def __init__(self, charID, charName, callback): + threading.Thread.__init__(self) + + self.name = "CheckUpdate" + self.callback = callback + self.charID = charID + self.charName = charName + + def run(self): + try: + dbChar = eos.db.getCharacter(self.charID) + dbChar.defaultChar = self.charName + + api = EVEAPIConnection() + auth = api.auth(keyID=dbChar.apiID, vCode=dbChar.apiKey) + apiResult = auth.account.Characters() + charID = None + for char in apiResult.characters: + if char.name == self.charName: + charID = char.characterID + break + + if charID is None: + return + + sheet = auth.character(charID).CharacterSheet() + charInfo = api.eve.CharacterInfo(characterID=charID) + + dbChar.apiUpdateCharSheet(sheet.skills, charInfo.securityStatus) + self.callback[0](self.callback[1]) + except Exception: + self.callback[0](self.callback[1], sys.exc_info()) diff --git a/service/fit.py b/service/fit.py index 50955c6ae..e74e0b108 100644 --- a/service/fit.py +++ b/service/fit.py @@ -19,6 +19,8 @@ import copy from logbook import Logger +from time import time +import datetime import eos.db from eos.saveddata.booster import Booster as es_Booster @@ -72,6 +74,8 @@ class Fit(object): "exportCharges": True, "openFitInNew": False, "priceSystem": "Jita", + "showShipBrowserTooltip": True, + "marketSearchDelay": 250 } self.serviceFittingOptions = SettingsProvider.getInstance().getSettings( @@ -79,35 +83,49 @@ class Fit(object): @staticmethod def getAllFits(): + pyfalog.debug("Fetching all fits") fits = eos.db.getFitList() return fits @staticmethod def getFitsWithShip(shipID): """ Lists fits of shipID, used with shipBrowser """ + pyfalog.debug("Fetching all fits for ship ID: {0}", shipID) fits = eos.db.getFitsWithShip(shipID) names = [] for fit in fits: - names.append((fit.ID, fit.name, fit.booster, fit.timestamp)) + names.append((fit.ID, fit.name, fit.booster, fit.modified or fit.created or datetime.datetime.fromtimestamp(fit.timestamp), fit.notes)) return names @staticmethod - def getBoosterFits(): - """ Lists fits flagged as booster """ - fits = eos.db.getBoosterFits() - names = [] - for fit in fits: - names.append((fit.ID, fit.name, fit.shipID)) + def getRecentFits(): + """ Fetches recently modified fits, used with shipBrowser """ + pyfalog.debug("Fetching recent fits") + fits = eos.db.getRecentFits() + returnInfo = [] - return names + for fit in fits: + item = eos.db.getItem(fit[1]) + returnInfo.append((fit[0], fit[2], fit[3] or fit[4] or datetime.datetime.fromtimestamp(fit[5]), item, fit[6])) + # ID name timestamps item notes + + return returnInfo + + @staticmethod + def getFitsWithModules(typeIDs): + """ Lists fits flagged as booster """ + fits = eos.db.getFitsWithModules(typeIDs) + return fits @staticmethod def countAllFits(): + pyfalog.debug("Getting count of all fits.") return eos.db.countAllFits() @staticmethod def countFitsWithShip(stuff): + pyfalog.debug("Getting count of all fits for: {0}", stuff) count = eos.db.countFitsWithShip(stuff) return count @@ -117,6 +135,7 @@ class Fit(object): return fit.modules[pos] def newFit(self, shipID, name=None): + pyfalog.debug("Creating new fit for ID: {0}", shipID) try: ship = es_Ship(eos.db.getItem(shipID)) except ValueError: @@ -133,18 +152,21 @@ class Fit(object): @staticmethod def toggleBoostFit(fitID): + pyfalog.debug("Toggling as booster for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) fit.booster = not fit.booster eos.db.commit() @staticmethod def renameFit(fitID, newName): + pyfalog.debug("Renaming fit ({0}) to: {1}", fitID, newName) fit = eos.db.getFit(fitID) fit.name = newName eos.db.commit() @staticmethod def deleteFit(fitID): + pyfalog.debug("Deleting fit for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) eos.db.remove(fit) @@ -157,6 +179,7 @@ class Fit(object): @staticmethod def copyFit(fitID): + pyfalog.debug("Creating copy of fit ID: {0}", fitID) fit = eos.db.getFit(fitID) newFit = copy.deepcopy(fit) eos.db.save(newFit) @@ -164,6 +187,7 @@ class Fit(object): @staticmethod def clearFit(fitID): + pyfalog.debug("Clearing fit for fit ID: {0}", fitID) if fitID is None: return None @@ -171,7 +195,14 @@ class Fit(object): fit.clear() return fit + @staticmethod + def editNotes(fitID, notes): + fit = eos.db.getFit(fitID) + fit.notes = notes + eos.db.commit() + def toggleFactorReload(self, fitID): + pyfalog.debug("Toggling factor reload for fit ID: {0}", fitID) if fitID is None: return None @@ -181,6 +212,7 @@ class Fit(object): self.recalc(fit) def switchFit(self, fitID): + pyfalog.debug("Switching fit to fit ID: {0}", fitID) if fitID is None: return None @@ -195,7 +227,9 @@ class Fit(object): fit.damagePattern = self.pattern eos.db.commit() - self.recalc(fit, withBoosters=True) + + if not fit.calculated: + self.recalc(fit) def getFit(self, fitID, projected=False, basic=False): """ @@ -204,6 +238,7 @@ class Fit(object): Projected is a recursion flag that is set to reduce recursions into projected fits Basic is a flag to simply return the fit without any other processing """ + pyfalog.debug("Getting fit for fit ID: {0}", fitID) if fitID is None: return None fit = eos.db.getFit(fitID) @@ -217,9 +252,15 @@ class Fit(object): if not projected: for fitP in fit.projectedFits: self.getFit(fitP.ID, projected=True) - self.recalc(fit, withBoosters=True) + self.recalc(fit) fit.fill() + # this will loop through modules and set their restriction flag (set in m.fit()) + if fit.ignoreRestrictions: + for mod in fit.modules: + if not mod.isEmpty: + mod.fits(fit) + # Check that the states of all modules are valid self.checkStates(fit, None) @@ -229,15 +270,23 @@ class Fit(object): @staticmethod def searchFits(name): + pyfalog.debug("Searching for fit: {0}", name) results = eos.db.searchFits(name) fits = [] + for fit in results: fits.append(( - fit.ID, fit.name, fit.ship.item.ID, fit.ship.item.name, fit.booster, - fit.timestamp)) + fit.ID, + fit.name, + fit.ship.item.ID, + fit.ship.item.name, + fit.booster, + fit.modifiedCoalesce, + fit.notes)) return fits def addImplant(self, fitID, itemID, recalc=True): + pyfalog.debug("Adding implant to fit ({0}) for item ID: {1}", fitID, itemID) if fitID is None: return False @@ -255,6 +304,7 @@ class Fit(object): return True def removeImplant(self, fitID, position, recalc=True): + pyfalog.debug("Removing implant from position ({0}) for fit ID: {1}", position, fitID) if fitID is None: return False @@ -265,7 +315,8 @@ class Fit(object): self.recalc(fit) return True - def addBooster(self, fitID, itemID): + def addBooster(self, fitID, itemID, recalc=True): + pyfalog.debug("Adding booster ({0}) to fit ID: {1}", itemID, fitID) if fitID is None: return False @@ -278,20 +329,24 @@ class Fit(object): return False fit.boosters.append(booster) - self.recalc(fit) + if recalc: + self.recalc(fit) return True - def removeBooster(self, fitID, position): + def removeBooster(self, fitID, position, recalc=True): + pyfalog.debug("Removing booster from position ({0}) for fit ID: {1}", position, fitID) if fitID is None: return False fit = eos.db.getFit(fitID) booster = fit.boosters[position] fit.boosters.remove(booster) - self.recalc(fit) + if recalc: + self.recalc(fit) return True def project(self, fitID, thing): + pyfalog.debug("Projecting fit ({0}) onto: {1}", fitID, thing) if fitID is None: return @@ -341,6 +396,7 @@ class Fit(object): return True def addCommandFit(self, fitID, thing): + pyfalog.debug("Projecting command fit ({0}) onto: {1}", fitID, thing) if fitID is None: return @@ -360,6 +416,7 @@ class Fit(object): return True def toggleProjected(self, fitID, thing, click): + pyfalog.debug("Toggling projected on fit ({0}) for: {1}", fitID, thing) fit = eos.db.getFit(fitID) if isinstance(thing, es_Drone): if thing.amountActive == 0 and thing.canBeApplied(fit): @@ -381,6 +438,7 @@ class Fit(object): self.recalc(fit) def toggleCommandFit(self, fitID, thing): + pyfalog.debug("Toggle command fit ({0}) for: {1}", fitID, thing) fit = eos.db.getFit(fitID) commandInfo = thing.getCommandInfo(fitID) if commandInfo: @@ -391,6 +449,7 @@ class Fit(object): def changeAmount(self, fitID, projected_fit, amount): """Change amount of projected fits""" + pyfalog.debug("Changing fit ({0}) for projected fit ({1}) to new amount: {2}", fitID, projected_fit.getProjectionInfo(fitID), amount) fit = eos.db.getFit(fitID) amount = min(20, max(1, amount)) # 1 <= a <= 20 projectionInfo = projected_fit.getProjectionInfo(fitID) @@ -401,6 +460,7 @@ class Fit(object): self.recalc(fit) def changeActiveFighters(self, fitID, fighter, amount): + pyfalog.debug("Changing active fighters ({0}) for fit ({1}) to amount: {2}", fighter.itemID, fitID, amount) fit = eos.db.getFit(fitID) fighter.amountActive = amount @@ -408,6 +468,7 @@ class Fit(object): self.recalc(fit) def removeProjected(self, fitID, thing): + pyfalog.debug("Removing projection on fit ({0}) from: {1}", fitID, thing) fit = eos.db.getFit(fitID) if isinstance(thing, es_Drone): fit.projectedDrones.remove(thing) @@ -423,6 +484,7 @@ class Fit(object): self.recalc(fit) def removeCommand(self, fitID, thing): + pyfalog.debug("Removing command projection from fit ({0}) for: {1}", fitID, thing) fit = eos.db.getFit(fitID) del fit.__commandFits[thing.ID] @@ -430,6 +492,7 @@ class Fit(object): self.recalc(fit) def appendModule(self, fitID, itemID): + pyfalog.debug("Appending module for fit ({0}) using item: {1}", fitID, itemID) fit = eos.db.getFit(fitID) item = eos.db.getItem(itemID, eager=("attributes", "group.category")) try: @@ -461,6 +524,7 @@ class Fit(object): return None def removeModule(self, fitID, position): + pyfalog.debug("Removing module from position ({0}) for fit ID: {1}", position, fitID) fit = eos.db.getFit(fitID) if fit.modules[position].isEmpty: return None @@ -474,6 +538,7 @@ class Fit(object): return numSlots != len(fit.modules) def changeModule(self, fitID, position, newItemID): + pyfalog.debug("Changing position of module from position ({0}) for fit ID: {1}", position, fitID) fit = eos.db.getFit(fitID) # Dummy it out in case the next bit fails @@ -512,6 +577,7 @@ class Fit(object): sanity checks as opposed to the GUI View. This is different than how the normal .swapModules() does things, which is mostly a blind swap. """ + pyfalog.debug("Moving cargo item to module for fit ID: {1}", fitID) fit = eos.db.getFit(fitID) module = fit.modules[moduleIdx] @@ -559,6 +625,7 @@ class Fit(object): @staticmethod def swapModules(fitID, src, dst): + pyfalog.debug("Swapping modules from source ({0}) to destination ({1}) for fit ID: {1}", src, dst, fitID) fit = eos.db.getFit(fitID) # Gather modules srcMod = fit.modules[src] @@ -578,6 +645,7 @@ class Fit(object): This will overwrite dst! Checking for empty module must be done at a higher level """ + pyfalog.debug("Cloning modules from source ({0}) to destination ({1}) for fit ID: {1}", src, dst, fitID) fit = eos.db.getFit(fitID) # Gather modules srcMod = fit.modules[src] @@ -598,6 +666,7 @@ class Fit(object): Adds cargo via typeID of item. If replace = True, we replace amount with given parameter, otherwise we increment """ + pyfalog.debug("Adding cargo ({0}) fit ID: {1}", itemID, fitID) if fitID is None: return False @@ -630,6 +699,7 @@ class Fit(object): return True def removeCargo(self, fitID, position): + pyfalog.debug("Removing cargo from position ({0}) fit ID: {1}", position, fitID) if fitID is None: return False @@ -639,7 +709,8 @@ class Fit(object): self.recalc(fit) return True - def addFighter(self, fitID, itemID): + def addFighter(self, fitID, itemID, recalc=True): + pyfalog.debug("Adding fighters ({0}) to fit ID: {1}", itemID, fitID) if fitID is None: return False @@ -680,21 +751,25 @@ class Fit(object): return False eos.db.commit() - self.recalc(fit) + if recalc: + self.recalc(fit) return True else: return False - def removeFighter(self, fitID, i): + def removeFighter(self, fitID, i, recalc=True): + pyfalog.debug("Removing fighters from fit ID: {0}", fitID) fit = eos.db.getFit(fitID) f = fit.fighters[i] fit.fighters.remove(f) eos.db.commit() - self.recalc(fit) + if recalc: + self.recalc(fit) return True - def addDrone(self, fitID, itemID, numDronesToAdd=1): + def addDrone(self, fitID, itemID, numDronesToAdd=1, recalc=True): + pyfalog.debug("Adding {0} drones ({1}) to fit ID: {2}", numDronesToAdd, itemID, fitID) if fitID is None: return False @@ -715,12 +790,14 @@ class Fit(object): return False drone.amount += numDronesToAdd eos.db.commit() - self.recalc(fit) + if recalc: + self.recalc(fit) return True else: return False def mergeDrones(self, fitID, d1, d2, projected=False): + pyfalog.debug("Merging drones on fit ID: {0}", fitID) if fitID is None: return False @@ -747,6 +824,7 @@ class Fit(object): @staticmethod def splitDrones(fit, d, amount, l): + pyfalog.debug("Splitting drones for fit ID: {0}", fit) total = d.amount active = d.amountActive > 0 d.amount = amount @@ -759,6 +837,7 @@ class Fit(object): eos.db.commit() def splitProjectedDroneStack(self, fitID, d, amount): + pyfalog.debug("Splitting projected drone stack for fit ID: {0}", fitID) if fitID is None: return False @@ -766,13 +845,15 @@ class Fit(object): self.splitDrones(fit, d, amount, fit.projectedDrones) def splitDroneStack(self, fitID, d, amount): + pyfalog.debug("Splitting drone stack for fit ID: {0}", fitID) if fitID is None: return False fit = eos.db.getFit(fitID) self.splitDrones(fit, d, amount, fit.drones) - def removeDrone(self, fitID, i, numDronesToRemove=1): + def removeDrone(self, fitID, i, numDronesToRemove=1, recalc=True): + pyfalog.debug("Removing {0} drones for fit ID: {1}", numDronesToRemove, fitID) fit = eos.db.getFit(fitID) d = fit.drones[i] d.amount -= numDronesToRemove @@ -783,10 +864,12 @@ class Fit(object): del fit.drones[i] eos.db.commit() - self.recalc(fit) + if recalc: + self.recalc(fit) return True def toggleDrone(self, fitID, i): + pyfalog.debug("Toggling drones for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) d = fit.drones[i] if d.amount == d.amountActive: @@ -799,6 +882,7 @@ class Fit(object): return True def toggleFighter(self, fitID, i): + pyfalog.debug("Toggling fighters for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) f = fit.fighters[i] f.active = not f.active @@ -808,6 +892,7 @@ class Fit(object): return True def toggleImplant(self, fitID, i): + pyfalog.debug("Toggling implant for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) implant = fit.implants[i] implant.active = not implant.active @@ -817,6 +902,7 @@ class Fit(object): return True def toggleImplantSource(self, fitID, source): + pyfalog.debug("Toggling implant source for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) fit.implantSource = source @@ -824,7 +910,23 @@ class Fit(object): self.recalc(fit) return True + def toggleRestrictionIgnore(self, fitID): + pyfalog.debug("Toggling restriction ignore for fit ID: {0}", fitID) + fit = eos.db.getFit(fitID) + fit.ignoreRestrictions = not fit.ignoreRestrictions + + # remove invalid modules when switching back to enabled fitting restrictions + if not fit.ignoreRestrictions: + for m in fit.modules: + if not m.isEmpty and not m.fits(fit): + self.removeModule(fit.ID, m.modPosition) + + eos.db.commit() + self.recalc(fit) + return True + def toggleBooster(self, fitID, i): + pyfalog.debug("Toggling booster for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) booster = fit.boosters[i] booster.active = not booster.active @@ -834,12 +936,14 @@ class Fit(object): return True def toggleFighterAbility(self, fitID, ability): + pyfalog.debug("Toggling fighter ability for fit ID: {0}", fitID) fit = eos.db.getFit(fitID) ability.active = not ability.active eos.db.commit() self.recalc(fit) def changeChar(self, fitID, charID): + pyfalog.debug("Changing character ({0}) for fit ID: {1}", charID, fitID) if fitID is None or charID is None: if charID is not None: self.character = Character.getInstance().all5() @@ -855,6 +959,7 @@ class Fit(object): return eos.db.getItem(itemID).category.name == "Charge" def setAmmo(self, fitID, ammoID, modules): + pyfalog.debug("Set ammo for fit ID: {0}", fitID) if fitID is None: return @@ -869,6 +974,7 @@ class Fit(object): @staticmethod def getTargetResists(fitID): + pyfalog.debug("Get target resists for fit ID: {0}", fitID) if fitID is None: return @@ -876,6 +982,7 @@ class Fit(object): return fit.targetResists def setTargetResists(self, fitID, pattern): + pyfalog.debug("Set target resist for fit ID: {0}", fitID) if fitID is None: return @@ -887,6 +994,7 @@ class Fit(object): @staticmethod def getDamagePattern(fitID): + pyfalog.debug("Get damage pattern for fit ID: {0}", fitID) if fitID is None: return @@ -894,6 +1002,7 @@ class Fit(object): return fit.damagePattern def setDamagePattern(self, fitID, pattern): + pyfalog.debug("Set damage pattern for fit ID: {0}", fitID) if fitID is None: return @@ -904,6 +1013,7 @@ class Fit(object): self.recalc(fit) def setMode(self, fitID, mode): + pyfalog.debug("Set mode for fit ID: {0}", fitID) if fitID is None: return @@ -914,6 +1024,7 @@ class Fit(object): self.recalc(fit) def setAsPattern(self, fitID, ammo): + pyfalog.debug("Set as pattern for fit ID: {0}", fitID) if fitID is None: return @@ -931,6 +1042,7 @@ class Fit(object): self.recalc(fit) def checkStates(self, fit, base): + pyfalog.debug("Check states for fit ID: {0}", fit) changed = False for mod in fit.modules: if mod != base: @@ -955,6 +1067,7 @@ class Fit(object): self.recalc(fit) def toggleModulesState(self, fitID, base, modules, click): + pyfalog.debug("Toggle module state for fit ID: {0}", fitID) changed = False proposedState = self.__getProposedState(base, click) @@ -994,6 +1107,7 @@ class Fit(object): State.ONLINE: State.OFFLINE} def __getProposedState(self, mod, click, proposedState=None): + pyfalog.debug("Get proposed state for module.") if mod.slot == Slot.SUBSYSTEM or mod.isEmpty: return State.ONLINE @@ -1021,6 +1135,7 @@ class Fit(object): return currState def refreshFit(self, fitID): + pyfalog.debug("Refresh fit for fit ID: {0}", fitID) if fitID is None: return None @@ -1028,10 +1143,13 @@ class Fit(object): eos.db.commit() self.recalc(fit) - def recalc(self, fit, withBoosters=True): + def recalc(self, fit): + start_time = time() pyfalog.info("=" * 10 + "recalc" + "=" * 10) if fit.factorReload is not self.serviceFittingOptions["useGlobalForceReload"]: fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"] fit.clear() - fit.calculateModifiedAttributes(withBoosters=False) + fit.calculateModifiedAttributes() + + pyfalog.info("=" * 10 + "recalc time: " + str(time() - start_time) + "=" * 10) diff --git a/service/market.py b/service/market.py index e311c94e5..49f0d88d2 100644 --- a/service/market.py +++ b/service/market.py @@ -30,11 +30,10 @@ import config import eos.db from service import conversions from service.settings import SettingsProvider -from service.price import Price from eos.gamedata import Category as types_Category, Group as types_Group, Item as types_Item, MarketGroup as types_MarketGroup, \ MetaGroup as types_MetaGroup, MetaType as types_MetaType -from eos.saveddata.price import Price as types_Price + try: from collections import OrderedDict @@ -85,48 +84,6 @@ class ShipBrowserWorkerThread(threading.Thread): pyfalog.critical(e) -class PriceWorkerThread(threading.Thread): - def __init__(self): - threading.Thread.__init__(self) - self.name = "PriceWorker" - pyfalog.debug("Initialize PriceWorkerThread.") - - def run(self): - pyfalog.debug("Run start") - self.queue = Queue.Queue() - self.wait = {} - self.processUpdates() - pyfalog.debug("Run end") - - def processUpdates(self): - queue = self.queue - while True: - # Grab our data - callback, requests = queue.get() - - # Grab prices, this is the time-consuming part - if len(requests) > 0: - Price.fetchPrices(requests) - - wx.CallAfter(callback) - queue.task_done() - - # After we fetch prices, go through the list of waiting items and call their callbacks - for price in requests: - callbacks = self.wait.pop(price.typeID, None) - if callbacks: - for callback in callbacks: - wx.CallAfter(callback) - - def trigger(self, prices, callbacks): - self.queue.put((callbacks, prices)) - - def setToWait(self, itemID, callback): - if itemID not in self.wait: - self.wait[itemID] = [] - self.wait[itemID].append(callback) - - class SearchWorkerThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) @@ -180,7 +137,6 @@ class Market(object): instance = None def __init__(self): - self.priceCache = {} # Init recently used module storage serviceMarketRecentlyUsedModules = {"pyfaMarketRecentlyUsedModules": []} @@ -188,11 +144,6 @@ class Market(object): self.serviceMarketRecentlyUsedModules = SettingsProvider.getInstance().getSettings( "pyfaMarketRecentlyUsedModules", serviceMarketRecentlyUsedModules) - # Start price fetcher - self.priceWorkerThread = PriceWorkerThread() - self.priceWorkerThread.daemon = True - self.priceWorkerThread.start() - # Thread which handles search self.searchWorkerThread = SearchWorkerThread() self.searchWorkerThread.daemon = True @@ -229,6 +180,7 @@ class Market(object): "Apotheosis" : self.les_grp, # 5th EVE anniversary present "Zephyr" : self.les_grp, # 2010 new year gift "Primae" : self.les_grp, # Promotion of planetary interaction + "Council Diplomatic Shuttle" : self.les_grp, # CSM X celebration "Freki" : self.les_grp, # AT7 prize "Mimir" : self.les_grp, # AT7 prize "Utu" : self.les_grp, # AT8 prize @@ -274,7 +226,6 @@ class Market(object): "Guristas Shuttle" : False, "Mobile Decoy Unit" : False, # Seems to be left over test mod for deployables "Tournament Micro Jump Unit" : False, # Normally seen only on tournament arenas - "Council Diplomatic Shuttle" : False, # CSM X celebration "Civilian Gatling Railgun" : True, "Civilian Gatling Pulse Laser" : True, "Civilian Gatling Autocannon" : True, @@ -855,60 +806,6 @@ class Market(object): filtered = set(filter(lambda item: self.getMetaGroupIdByItem(item) in metas, items)) return filtered - def getPriceNow(self, typeID): - """Get price for provided typeID""" - price = self.priceCache.get(typeID) - if price is None: - price = eos.db.getPrice(typeID) - if price is None: - price = types_Price(typeID) - eos.db.add(price) - - self.priceCache[typeID] = price - - return price - - def getPricesNow(self, typeIDs): - """Return map of calls to get price against list of typeIDs""" - return map(self.getPrice, typeIDs) - - def getPrices(self, typeIDs, callback): - """Get prices for multiple typeIDs""" - requests = [] - for typeID in typeIDs: - price = self.getPriceNow(typeID) - requests.append(price) - - def cb(): - try: - callback(requests) - except Exception as e: - pyfalog.critical("Callback failed.") - pyfalog.critical(e) - eos.db.commit() - - self.priceWorkerThread.trigger(requests, cb) - - def waitForPrice(self, item, callback): - """ - Wait for prices to be fetched and callback when finished. This is used with the column prices for modules. - Instead of calling them individually, we set them to wait until the entire fit price is called and calculated - (see GH #290) - """ - - def cb(): - try: - callback(item) - except Exception as e: - pyfalog.critical("Callback failed.") - pyfalog.critical(e) - - self.priceWorkerThread.setToWait(item.ID, cb) - - def clearPriceCache(self): - self.priceCache.clear() - eos.db.clearPrices() - def getSystemWideEffects(self): """ Get dictionary with system-wide effects diff --git a/service/port.py b/service/port.py index 3e923ed0f..718bf41a7 100644 --- a/service/port.py +++ b/service/port.py @@ -46,6 +46,8 @@ from eos.saveddata.ship import Ship from eos.saveddata.citadel import Citadel from eos.saveddata.fit import Fit from service.market import Market +from utils.strfunctions import sequential_rep, replace_ltgt +from abc import ABCMeta, abstractmethod if 'wxMac' not in wx.PlatformInfo or ('wxMac' in wx.PlatformInfo and wx.VERSION >= (3, 0)): from service.crest import Crest @@ -70,9 +72,152 @@ INV_FLAG_CARGOBAY = 5 INV_FLAG_DRONEBAY = 87 INV_FLAG_FIGHTER = 158 +# 2017/04/05 NOTE: simple validation, for xml file +RE_XML_START = r'<\?xml\s+version="1.0"\s*\?>' + +# -- 170327 Ignored description -- +RE_LTGT = "&(lt|gt);" +L_MARK = "<localized hint="" +# <localized hint="([^"]+)">([^\*]+)\*<\/localized> +LOCALIZED_PATTERN = re.compile(r'([^\*]+)\*') + + +def _extract_match(t): + m = LOCALIZED_PATTERN.match(t) + # hint attribute, text content + return m.group(1), m.group(2) + + +def _resolve_ship(fitting, sMkt, b_localized): + # type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.fit.Fit + """ NOTE: Since it is meaningless unless a correct ship object can be constructed, + process flow changed + """ + # ------ Confirm ship + # Maelstrom + shipType = fitting.getElementsByTagName("shipType").item(0).getAttribute("value") + anything = None + if b_localized: + # expect an official name, emergency cache + shipType, anything = _extract_match(shipType) + + limit = 2 + ship = None + while True: + must_retry = False + try: + try: + ship = Ship(sMkt.getItem(shipType)) + except ValueError: + ship = Citadel(sMkt.getItem(shipType)) + except Exception as e: + pyfalog.warning("Caught exception on _resolve_ship") + pyfalog.error(e) + limit -= 1 + if limit is 0: + break + shipType = anything + must_retry = True + if not must_retry: + break + + if ship is None: + raise Exception("cannot resolve ship type.") + + fitobj = Fit(ship=ship) + # ------ Confirm fit name + anything = fitting.getAttribute("name") + # 2017/03/29 NOTE: + # if fit name contained "<" or ">" then reprace to named html entity by EVE client + # if re.search(RE_LTGT, anything): + if "<" in anything or ">" in anything: + anything = replace_ltgt(anything) + fitobj.name = anything + + return fitobj + + +def _resolve_module(hardware, sMkt, b_localized): + # type: (xml.dom.minidom.Element, service.market.Market, bool) -> eos.saveddata.module.Module + moduleName = hardware.getAttribute("type") + emergency = None + if b_localized: + # expect an official name, emergency cache + moduleName, emergency = _extract_match(moduleName) + + item = None + limit = 2 + while True: + must_retry = False + try: + item = sMkt.getItem(moduleName, eager="group.category") + except Exception as e: + pyfalog.warning("Caught exception on _resolve_module") + pyfalog.error(e) + limit -= 1 + if limit is 0: + break + moduleName = emergency + must_retry = True + if not must_retry: + break + return item + + +class UserCancelException(Exception): + """when user cancel on port processing.""" + pass + + +class IPortUser: + + __metaclass__ = ABCMeta + + ID_PULSE = 1 + # Pulse the progress bar + ID_UPDATE = ID_PULSE << 1 + # Replace message with data: update messate + ID_DONE = ID_PULSE << 2 + # open fits: import process done + ID_ERROR = ID_PULSE << 3 + # display error: raise some error + + PROCESS_IMPORT = ID_PULSE << 4 + # means import process. + PROCESS_EXPORT = ID_PULSE << 5 + # means import process. + + @abstractmethod + def on_port_processing(self, action, data=None): + """ + While importing fits from file, the logic calls back to this function to + update progress bar to show activity. XML files can contain multiple + ships with multiple fits, whereas EFT cfg files contain many fits of + a single ship. When iterating through the files, we update the message + when we start a new file, and then Pulse the progress bar with every fit + that is processed. + + action : a flag that lets us know how to deal with :data + None: Pulse the progress bar + 1: Replace message with data + other: Close dialog and handle based on :action (-1 open fits, -2 display error) + """ + + """return: True is continue process, False is cancel.""" + pass + + def on_port_process_start(self): + pass + class Port(object): + """ + 2017/03/31 NOTE: About change + 1. want to keep the description recorded in fit + 2. i think should not write wx.CallAfter in here + """ instance = None + __tag_replace_flag = True @classmethod def getInstance(cls): @@ -81,20 +226,44 @@ class Port(object): return cls.instance + @classmethod + def set_tag_replace(cls, b): + cls.__tag_replace_flag = b + + @classmethod + def is_tag_replace(cls): + # might there is a person who wants to hold tags. + # (item link in EVE client etc. When importing again to EVE) + return cls.__tag_replace_flag + @staticmethod - def backupFits(path, callback): + def backupFits(path, iportuser): pyfalog.debug("Starting backup fits thread.") - thread = FitBackupThread(path, callback) - thread.start() +# thread = FitBackupThread(path, callback) +# thread.start() + threading.Thread( + target=PortProcessing.backupFits, + args=(path, iportuser) + ).start() @staticmethod - def importFitsThreaded(paths, callback): + def importFitsThreaded(paths, iportuser): + # type: (tuple, IPortUser) -> None + """ + :param paths: fits data file path list. + :param iportuser: IPortUser implemented class. + :rtype: None + """ pyfalog.debug("Starting import fits thread.") - thread = FitImportThread(paths, callback) - thread.start() +# thread = FitImportThread(paths, iportuser) +# thread.start() + threading.Thread( + target=PortProcessing.importFitsFromFile, + args=(paths, iportuser) + ).start() @staticmethod - def importFitFromFiles(paths, callback=None): + def importFitFromFiles(paths, iportuser=None): """ Imports fits from file(s). First processes all provided paths and stores assembled fits into a list. This allows us to call back to the GUI as @@ -104,99 +273,109 @@ class Port(object): defcodepage = locale.getpreferredencoding() sFit = svcFit.getInstance() - fits = [] - for path in paths: - if callback: # Pulse - pyfalog.debug("Processing file:\n{0}", path) - wx.CallAfter(callback, 1, "Processing file:\n%s" % path) + fit_list = [] + try: + for path in paths: + if iportuser: # Pulse + msg = "Processing file:\n%s" % path + pyfalog.debug(msg) + PortProcessing.notify(iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, msg) + # wx.CallAfter(callback, 1, msg) - file_ = open(path, "r") - srcString = file_.read() + with open(path, "r") as file_: + srcString = file_.read() - if len(srcString) == 0: # ignore blank files - pyfalog.debug("File is blank.") - continue + if len(srcString) == 0: # ignore blank files + pyfalog.debug("File is blank.") + continue - codec_found = None - # If file had ANSI encoding, decode it to unicode using detection - # of BOM header or if there is no header try default - # codepage then fallback to utf-16, cp1252 + codec_found = None + # If file had ANSI encoding, decode it to unicode using detection + # of BOM header or if there is no header try default + # codepage then fallback to utf-16, cp1252 - if isinstance(srcString, str): - savebom = None + if isinstance(srcString, str): + savebom = None - encoding_map = ( - ('\xef\xbb\xbf', 'utf-8'), - ('\xff\xfe\0\0', 'utf-32'), - ('\0\0\xfe\xff', 'UTF-32BE'), - ('\xff\xfe', 'utf-16'), - ('\xfe\xff', 'UTF-16BE')) + encoding_map = ( + ('\xef\xbb\xbf', 'utf-8'), + ('\xff\xfe\0\0', 'utf-32'), + ('\0\0\xfe\xff', 'UTF-32BE'), + ('\xff\xfe', 'utf-16'), + ('\xfe\xff', 'UTF-16BE')) - for bom, encoding in encoding_map: - if srcString.startswith(bom): - codec_found = encoding - savebom = bom + for bom, encoding in encoding_map: + if srcString.startswith(bom): + codec_found = encoding + savebom = bom + + if codec_found is None: + pyfalog.info("Unicode BOM not found in file {0}.", path) + attempt_codecs = (defcodepage, "utf-8", "utf-16", "cp1252") + + for page in attempt_codecs: + try: + pyfalog.info("Attempting to decode file {0} using {1} page.", path, page) + srcString = unicode(srcString, page) + codec_found = page + pyfalog.info("File {0} decoded using {1} page.", path, page) + except UnicodeDecodeError: + pyfalog.info("Error unicode decoding {0} from page {1}, trying next codec", path, page) + else: + break + else: + pyfalog.info("Unicode BOM detected in {0}, using {1} page.", path, codec_found) + srcString = unicode(srcString[len(savebom):], codec_found) + + else: + # nasty hack to detect other transparent utf-16 loading + if srcString[0] == '<' and 'utf-16' in srcString[:128].lower(): + codec_found = "utf-16" + else: + codec_found = "utf-8" if codec_found is None: - pyfalog.info("Unicode BOM not found in file {0}.", path) - attempt_codecs = (defcodepage, "utf-8", "utf-16", "cp1252") + return False, "Proper codec could not be established for %s" % path - for page in attempt_codecs: - try: - pyfalog.info("Attempting to decode file {0} using {1} page.", path, page) - srcString = unicode(srcString, page) - codec_found = page - pyfalog.info("File {0} decoded using {1} page.", path, page) - except UnicodeDecodeError: - pyfalog.info("Error unicode decoding {0} from page {1}, trying next codec", path, page) - else: - break - else: - pyfalog.info("Unicode BOM detected in {0}, using {1} page.", path, codec_found) - srcString = unicode(srcString[len(savebom):], codec_found) + try: + _, fitsImport = Port.importAuto(srcString, path, iportuser=iportuser, encoding=codec_found) + fit_list += fitsImport + except xml.parsers.expat.ExpatError: + pyfalog.warning("Malformed XML in:\n{0}", path) + return False, "Malformed XML in %s" % path - else: - # nasty hack to detect other transparent utf-16 loading - if srcString[0] == '<' and 'utf-16' in srcString[:128].lower(): - codec_found = "utf-16" - else: - codec_found = "utf-8" + # IDs = [] # NOTE: what use for IDs? + numFits = len(fit_list) + for idx, fit in enumerate(fit_list): + # Set some more fit attributes and save + fit.character = sFit.character + fit.damagePattern = sFit.pattern + fit.targetResists = sFit.targetResists + db.save(fit) + # IDs.append(fit.ID) + if iportuser: # Pulse + pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", idx + 1, numFits) + PortProcessing.notify( + iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, + "Processing complete, saving fits to database\n(%d/%d) %s" % (idx + 1, numFits, fit.ship.name) + ) - if codec_found is None: - return False, "Proper codec could not be established for %s" % path + except UserCancelException: + return False, "Processing has been canceled.\n" + except Exception as e: + pyfalog.critical("Unknown exception processing: {0}", path) + pyfalog.critical(e) + # TypeError: not all arguments converted during string formatting +# return False, "Unknown Error while processing {0}" % path + return False, "Unknown error while processing %s\n\n Error: %s" % (path, e.message) - try: - _, fitsImport = Port.importAuto(srcString, path, callback=callback, encoding=codec_found) - fits += fitsImport - except xml.parsers.expat.ExpatError: - pyfalog.warning("Malformed XML in:\n{0}", path) - return False, "Malformed XML in %s" % path - except Exception as e: - pyfalog.critical("Unknown exception processing: {0}", path) - pyfalog.critical(e) - return False, "Unknown Error while processing {0}" % path - - IDs = [] - numFits = len(fits) - for i, fit in enumerate(fits): - # Set some more fit attributes and save - fit.character = sFit.character - fit.damagePattern = sFit.pattern - fit.targetResists = sFit.targetResists - db.save(fit) - IDs.append(fit.ID) - if callback: # Pulse - pyfalog.debug("Processing complete, saving fits to database: {0}/{1}", i + 1, numFits) - wx.CallAfter( - callback, 1, - "Processing complete, saving fits to database\n(%d/%d)" % - (i + 1, numFits) - ) - - return True, fits + return True, fit_list @staticmethod def importFitFromBuffer(bufferStr, activeFit=None): + # type: (basestring, object) -> object + # TODO: catch the exception? + # activeFit is reserved?, bufferStr is unicode? (assume only clipboard string? sFit = svcFit.getInstance() _, fits = Port.importAuto(bufferStr, activeFit=activeFit) for fit in fits: @@ -228,7 +407,9 @@ class Port(object): fit['ship']['id'] = ofit.ship.item.ID fit['ship']['name'] = '' - fit['description'] = "" % ofit.ID + # 2017/03/29 NOTE: "<" or "<" is Ignored + # fit['description'] = "" % ofit.ID + fit['description'] = ofit.notes[:397] + '...' if len(ofit.notes) > 400 else ofit.notes if ofit.notes is not None else "" fit['items'] = [] slotNum = {} @@ -302,17 +483,18 @@ class Port(object): return json.dumps(fit) @classmethod - def importAuto(cls, string, path=None, activeFit=None, callback=None, encoding=None): + def importAuto(cls, string, path=None, activeFit=None, iportuser=None, encoding=None): + # type: (basestring, basestring, object, IPortUser, basestring) -> object # Get first line and strip space symbols of it to avoid possible detection errors firstLine = re.split("[\n\r]+", string.strip(), maxsplit=1)[0] firstLine = firstLine.strip() # If XML-style start of tag encountered, detect as XML - if re.match("<", firstLine): + if re.search(RE_XML_START, firstLine): if encoding: - return "XML", cls.importXml(string, callback, encoding) + return "XML", cls.importXml(string, iportuser, encoding) else: - return "XML", cls.importXml(string, callback) + return "XML", cls.importXml(string, iportuser) # If JSON-style start, parse as CREST/JSON if firstLine[0] == '{': @@ -323,7 +505,7 @@ class Port(object): if re.match("\[.*\]", firstLine) and path is not None: filename = os.path.split(path)[1] shipName = filename.rsplit('.')[0] - return "EFT Config", cls.importEftCfg(shipName, string, callback) + return "EFT Config", cls.importEftCfg(shipName, string, iportuser) # If no file is specified and there's comma between brackets, # consider that we have [ship, setup name] and detect like eft export format @@ -335,22 +517,26 @@ class Port(object): @staticmethod def importCrest(str_): - fit = json.loads(str_) - sMkt = Market.getInstance() - f = Fit() - f.name = fit['name'] + sMkt = Market.getInstance() + fitobj = Fit() + refobj = json.loads(str_) + items = refobj['items'] + # "<" and ">" is replace to "<", ">" by EVE client + fitobj.name = refobj['name'] + # 2017/03/29: read description + fitobj.notes = refobj['description'] try: + refobj = refobj['ship']['id'] try: - f.ship = Ship(sMkt.getItem(fit['ship']['id'])) + fitobj.ship = Ship(sMkt.getItem(refobj)) except ValueError: - f.ship = Citadel(sMkt.getItem(fit['ship']['id'])) + fitobj.ship = Citadel(sMkt.getItem(refobj)) except: pyfalog.warning("Caught exception in importCrest") return None - items = fit['items'] items.sort(key=lambda k: k['flag']) moduleList = [] @@ -360,14 +546,14 @@ class Port(object): if module['flag'] == INV_FLAG_DRONEBAY: d = Drone(item) d.amount = module['quantity'] - f.drones.append(d) + fitobj.drones.append(d) elif module['flag'] == INV_FLAG_CARGOBAY: c = Cargo(item) c.amount = module['quantity'] - f.cargo.append(c) + fitobj.cargo.append(c) elif module['flag'] == INV_FLAG_FIGHTER: fighter = Fighter(item) - f.fighters.append(fighter) + fitobj.fighters.append(fighter) else: try: m = Module(item) @@ -377,8 +563,8 @@ class Port(object): continue # Add subsystems before modules to make sure T3 cruisers have subsystems installed if item.category.name == "Subsystem": - if m.fits(f): - f.modules.append(m) + if m.fits(fitobj): + fitobj.modules.append(m) else: if m.isValidState(State.ACTIVE): m.state = State.ACTIVE @@ -390,13 +576,13 @@ class Port(object): continue # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(f) + svcFit.getInstance().recalc(fitobj) for module in moduleList: - if module.fits(f): - f.modules.append(module) + if module.fits(fitobj): + fitobj.modules.append(module) - return f + return fitobj @staticmethod def importDna(string): @@ -635,7 +821,7 @@ class Port(object): return fit @staticmethod - def importEftCfg(shipname, contents, callback=None): + def importEftCfg(shipname, contents, iportuser=None): """Handle import from EFT config store file""" # Check if we have such ship in database, bail if we don't @@ -648,7 +834,7 @@ class Port(object): # If client didn't take care of encoding file contents into Unicode, # do it using fallback encoding ourselves if isinstance(contents, str): - contents = unicode(contents, "cp1252") + contents = unicode(contents, locale.getpreferredencoding()) fits = [] # List for fits fitIndices = [] # List for starting line numbers for each fit @@ -671,14 +857,14 @@ class Port(object): try: # Create fit object - f = Fit() + fitobj = Fit() # Strip square brackets and pull out a fit name - f.name = fitLines[0][1:-1] + fitobj.name = fitLines[0][1:-1] # Assign ship to fitting try: - f.ship = Ship(sMkt.getItem(shipname)) + fitobj.ship = Ship(sMkt.getItem(shipname)) except ValueError: - f.ship = Citadel(sMkt.getItem(shipname)) + fitobj.ship = Citadel(sMkt.getItem(shipname)) moduleList = [] for x in range(1, len(fitLines)): @@ -689,6 +875,8 @@ class Port(object): # Parse line into some data we will need misc = re.match("(Drones|Implant|Booster)_(Active|Inactive)=(.+)", line) cargo = re.match("Cargohold=(.+)", line) + # 2017/03/27 NOTE: store description from EFT + description = re.match("Description=(.+)", line) if misc: entityType = misc.group(1) @@ -713,11 +901,11 @@ class Port(object): d.amountActive = droneAmount elif entityState == "Inactive": d.amountActive = 0 - f.drones.append(d) + fitobj.drones.append(d) elif droneItem.category.name == "Fighter": # EFT saves fighter as drones ft = Fighter(droneItem) ft.amount = int(droneAmount) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize - f.fighters.append(ft) + fitobj.fighters.append(ft) else: continue elif entityType == "Implant": @@ -735,7 +923,7 @@ class Port(object): imp.active = True elif entityState == "Inactive": imp.active = False - f.implants.append(imp) + fitobj.implants.append(imp) elif entityType == "Booster": # Bail if we can't get item or it's not from implant category try: @@ -752,7 +940,7 @@ class Port(object): b.active = True elif entityState == "Inactive": b.active = False - f.boosters.append(b) + fitobj.boosters.append(b) # If we don't have any prefixes, then it's a module elif cargo: cargoData = re.match("(.+),([0-9]+)", cargo.group(1)) @@ -767,7 +955,10 @@ class Port(object): # Add Cargo to the fitting c = Cargo(item) c.amount = cargoAmount - f.cargo.append(c) + fitobj.cargo.append(c) + # 2017/03/27 NOTE: store description from EFT + elif description: + fitobj.notes = description.group(1).replace("|", "\n") else: withCharge = re.match("(.+),(.+)", line) modName = withCharge.group(1) if withCharge else line @@ -784,10 +975,10 @@ class Port(object): # Add subsystems before modules to make sure T3 cruisers have subsystems installed if modItem.category.name == "Subsystem": - if m.fits(f): - f.modules.append(m) + if m.fits(fitobj): + fitobj.modules.append(m) else: - m.owner = f + m.owner = fitobj # Activate mod if it is activable if m.isValidState(State.ACTIVE): m.state = State.ACTIVE @@ -804,17 +995,21 @@ class Port(object): moduleList.append(m) # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(f) + svcFit.getInstance().recalc(fitobj) for module in moduleList: - if module.fits(f): - f.modules.append(module) + if module.fits(fitobj): + fitobj.modules.append(module) # Append fit to list of fits - fits.append(f) + fits.append(fitobj) + + if iportuser: # NOTE: Send current processing status + PortProcessing.notify( + iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, + "%s:\n%s" % (fitobj.ship.name, fitobj.name) + ) - if callback: - wx.CallAfter(callback, None) # Skip fit silently if we get an exception except Exception as e: pyfalog.error("Caught exception on fit.") @@ -824,93 +1019,100 @@ class Port(object): return fits @staticmethod - def importXml(text, callback=None, encoding="utf-8"): + def importXml(text, iportuser=None, encoding="utf-8"): + # type: (basestring, IPortUser, basestring) -> list[eos.saveddata.fit.Fit] sMkt = Market.getInstance() - doc = xml.dom.minidom.parseString(text.encode(encoding)) + # NOTE: + # When L_MARK is included at this point, + # Decided to be localized data + b_localized = L_MARK in text fittings = doc.getElementsByTagName("fittings").item(0) fittings = fittings.getElementsByTagName("fitting") - fits = [] + fit_list = [] + + for fitting in fittings: + fitobj = _resolve_ship(fitting, sMkt, b_localized) + # -- 170327 Ignored description -- + # read description from exported xml. (EVE client, EFT) + description = fitting.getElementsByTagName("description").item(0).getAttribute("value") + if description is None: + description = "" + elif len(description): + # convert
to "\n" and remove html tags. + if Port.is_tag_replace(): + description = replace_ltgt( + sequential_rep(description, r"<(br|BR)>", "\n", r"<[^<>]+>", "") + ) + fitobj.notes = description - for i, fitting in enumerate(fittings): - f = Fit() - f.name = fitting.getAttribute("name") - # Maelstrom - shipType = fitting.getElementsByTagName("shipType").item(0).getAttribute("value") - try: - try: - f.ship = Ship(sMkt.getItem(shipType)) - except ValueError: - f.ship = Citadel(sMkt.getItem(shipType)) - except Exception as e: - pyfalog.warning("Caught exception on importXml") - pyfalog.error(e) - continue hardwares = fitting.getElementsByTagName("hardware") moduleList = [] for hardware in hardwares: try: - moduleName = hardware.getAttribute("type") - try: - item = sMkt.getItem(moduleName, eager="group.category") - except Exception as e: - pyfalog.warning("Caught exception on importXml") - pyfalog.error(e) + item = _resolve_module(hardware, sMkt, b_localized) + if not item: continue - if item: - if item.category.name == "Drone": - d = Drone(item) - d.amount = int(hardware.getAttribute("qty")) - f.drones.append(d) - elif item.category.name == "Fighter": - ft = Fighter(item) - ft.amount = int(hardware.getAttribute("qty")) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize - f.fighters.append(ft) - elif hardware.getAttribute("slot").lower() == "cargo": - # although the eve client only support charges in cargo, third-party programs - # may support items or "refits" in cargo. Support these by blindly adding all - # cargo, not just charges - c = Cargo(item) - c.amount = int(hardware.getAttribute("qty")) - f.cargo.append(c) - else: - try: - m = Module(item) - # When item can't be added to any slot (unknown item or just charge), ignore it - except ValueError: - pyfalog.warning("item can't be added to any slot (unknown item or just charge), ignore it") - continue - # Add subsystems before modules to make sure T3 cruisers have subsystems installed - if item.category.name == "Subsystem": - if m.fits(f): - m.owner = f - f.modules.append(m) - else: - if m.isValidState(State.ACTIVE): - m.state = State.ACTIVE - moduleList.append(m) + if item.category.name == "Drone": + d = Drone(item) + d.amount = int(hardware.getAttribute("qty")) + fitobj.drones.append(d) + elif item.category.name == "Fighter": + ft = Fighter(item) + ft.amount = int(hardware.getAttribute("qty")) if ft.amount <= ft.fighterSquadronMaxSize else ft.fighterSquadronMaxSize + fitobj.fighters.append(ft) + elif hardware.getAttribute("slot").lower() == "cargo": + # although the eve client only support charges in cargo, third-party programs + # may support items or "refits" in cargo. Support these by blindly adding all + # cargo, not just charges + c = Cargo(item) + c.amount = int(hardware.getAttribute("qty")) + fitobj.cargo.append(c) + else: + try: + m = Module(item) + # When item can't be added to any slot (unknown item or just charge), ignore it + except ValueError: + pyfalog.warning("item can't be added to any slot (unknown item or just charge), ignore it") + continue + # Add subsystems before modules to make sure T3 cruisers have subsystems installed + if item.category.name == "Subsystem": + if m.fits(fitobj): + m.owner = fitobj + fitobj.modules.append(m) + else: + if m.isValidState(State.ACTIVE): + m.state = State.ACTIVE + + moduleList.append(m) except KeyboardInterrupt: pyfalog.warning("Keyboard Interrupt") continue # Recalc to get slot numbers correct for T3 cruisers - svcFit.getInstance().recalc(f) + svcFit.getInstance().recalc(fitobj) for module in moduleList: - if module.fits(f): - module.owner = f - f.modules.append(module) + if module.fits(fitobj): + module.owner = fitobj + fitobj.modules.append(module) - fits.append(f) - if callback: - wx.CallAfter(callback, None) + fit_list.append(fitobj) + if iportuser: # NOTE: Send current processing status + PortProcessing.notify( + iportuser, IPortUser.PROCESS_IMPORT | IPortUser.ID_UPDATE, + "Processing %s\n%s" % (fitobj.ship.name, fitobj.name) + ) - return fits + return fit_list @staticmethod def _exportEftBase(fit): + """Basically EFT format does not require blank lines + also, it's OK to arrange modules randomly? + """ offineSuffix = " /OFFLINE" export = "[%s, %s]\n" % (fit.ship.item.name, fit.name) stuff = {} @@ -1038,10 +1240,13 @@ class Port(object): return dna + "::" - @classmethod - def exportXml(cls, callback=None, *fits): + @staticmethod + def exportXml(iportuser=None, *fits): doc = xml.dom.minidom.Document() fittings = doc.createElement("fittings") + # fit count + fit_count = len(fits) + fittings.setAttribute("count", "%s" % fit_count) doc.appendChild(fittings) sFit = svcFit.getInstance() @@ -1051,10 +1256,22 @@ class Port(object): fitting.setAttribute("name", fit.name) fittings.appendChild(fitting) description = doc.createElement("description") - description.setAttribute("value", "") + # -- 170327 Ignored description -- + try: + notes = fit.notes # unicode + + if notes: + notes = notes[:397] + '...' if len(notes) > 400 else notes + + description.setAttribute( + "value", re.sub("(\r|\n|\r\n)+", "
", notes) if notes is not None else "" + ) + except Exception as e: + pyfalog.warning("read description is failed, msg=%s\n" % e.args) + fitting.appendChild(description) shipType = doc.createElement("shipType") - shipType.setAttribute("value", fit.ship.item.name) + shipType.setAttribute("value", fit.ship.name) fitting.appendChild(shipType) charges = {} @@ -1113,12 +1330,17 @@ class Port(object): hardware.setAttribute("slot", "cargo") hardware.setAttribute("type", name) fitting.appendChild(hardware) - except: - print("Failed on fitID: %d" % fit.ID) + except Exception as e: + # print("Failed on fitID: %d" % fit.ID) + pyfalog.error("Failed on fitID: %d, message: %s" % e.message) continue finally: - if callback: - wx.CallAfter(callback, i) + if iportuser: + PortProcessing.notify( + iportuser, IPortUser.PROCESS_EXPORT | IPortUser.ID_UPDATE, + (i, "convert to xml (%s/%s) %s" % (i + 1, fit_count, fit.ship.name)) + ) +# wx.CallAfter(callback, i, "(%s/%s) %s" % (i, fit_count, fit.ship.name)) return doc.toprettyxml() @@ -1169,37 +1391,33 @@ class Port(object): return export -class FitBackupThread(threading.Thread): - def __init__(self, path, callback): - threading.Thread.__init__(self) - self.path = path - self.callback = callback - - def run(self): - path = self.path - sFit = svcFit.getInstance() - sPort = Port.getInstance() - backedUpFits = sPort.exportXml(self.callback, *sFit.getAllFits()) - backupFile = open(path, "w", encoding="utf-8") - backupFile.write(backedUpFits) - backupFile.close() - +class PortProcessing(object): + """Port Processing class """ + @staticmethod + def backupFits(path, iportuser): + success = True + try: + iportuser.on_port_process_start() + backedUpFits = Port.exportXml(iportuser, *svcFit.getInstance().getAllFits()) + backupFile = open(path, "w", encoding="utf-8") + backupFile.write(backedUpFits) + backupFile.close() + except UserCancelException: + success = False # Send done signal to GUI - wx.CallAfter(self.callback, -1) +# wx.CallAfter(callback, -1, "Done.") + flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE + iportuser.on_port_processing(IPortUser.PROCESS_EXPORT | flag, + "User canceled or some error occurrence." if not success else "Done.") + @staticmethod + def importFitsFromFile(paths, iportuser): + iportuser.on_port_process_start() + success, result = Port.importFitFromFiles(paths, iportuser) + flag = IPortUser.ID_ERROR if not success else IPortUser.ID_DONE + iportuser.on_port_processing(IPortUser.PROCESS_IMPORT | flag, result) -class FitImportThread(threading.Thread): - def __init__(self, paths, callback): - threading.Thread.__init__(self) - self.paths = paths - self.callback = callback - - def run(self): - sPort = Port.getInstance() - success, result = sPort.importFitFromFiles(self.paths, self.callback) - - if not success: # there was an error during processing - pyfalog.error("Error while processing file import: {0}", result) - wx.CallAfter(self.callback, -2, result) - else: # Send done signal to GUI - wx.CallAfter(self.callback, -1, result) + @staticmethod + def notify(iportuser, flag, data): + if not iportuser.on_port_processing(flag, data): + raise UserCancelException diff --git a/service/price.py b/service/price.py index b2466c4c4..fdfe8ac2c 100644 --- a/service/price.py +++ b/service/price.py @@ -19,12 +19,17 @@ import time +import threading +import Queue from xml.dom import minidom +from logbook import Logger +import wx + from eos import db from service.network import Network, TimeoutError from service.fit import Fit -from logbook import Logger +from service.market import Market pyfalog = Logger(__name__) @@ -35,6 +40,8 @@ TIMEOUT = 15 * 60 # Network timeout delay for connection issues, 15 minutes class Price(object): + instance = None + systemsList = { "Jita": 30000142, "Amarr": 30002187, @@ -43,10 +50,17 @@ class Price(object): "Hek": 30002053 } + def __init__(self): + # Start price fetcher + self.priceWorkerThread = PriceWorkerThread() + self.priceWorkerThread.daemon = True + self.priceWorkerThread.start() + @classmethod - def invalidPrices(cls, prices): - for price in prices: - price.time = 0 + def getInstance(cls): + if cls.instance is None: + cls.instance = Price() + return cls.instance @classmethod def fetchPrices(cls, prices): @@ -113,6 +127,9 @@ class Price(object): priceobj.time = time.time() + VALIDITY priceobj.failed = None + # Update the DB. + db.commit() + # delete price from working dict del priceMap[typeID] @@ -124,6 +141,10 @@ class Price(object): priceobj = priceMap[typeID] priceobj.time = time.time() + TIMEOUT priceobj.failed = True + + # Update the DB. + db.commit() + del priceMap[typeID] except: # all other errors will pass and continue onward to the REREQUEST delay @@ -136,22 +157,100 @@ class Price(object): priceobj.time = time.time() + REREQUEST priceobj.failed = True + # Update the DB. + db.commit() + @classmethod def fitItemsList(cls, fit): # Compose a list of all the data we need & request it - typeIDs = [fit.ship.item.ID] + fit_items = [fit.ship.item] for mod in fit.modules: if not mod.isEmpty: - typeIDs.append(mod.itemID) + fit_items.append(mod.item) for drone in fit.drones: - typeIDs.append(drone.itemID) + fit_items.append(drone.item) for fighter in fit.fighters: - typeIDs.append(fighter.itemID) + fit_items.append(fighter.item) for cargo in fit.cargo: - typeIDs.append(cargo.itemID) + fit_items.append(cargo.item) - return typeIDs + for boosters in fit.boosters: + fit_items.append(boosters.item) + + for implants in fit.implants: + fit_items.append(implants.item) + + return list(set(fit_items)) + + def getPriceNow(self, objitem): + """Get price for provided typeID""" + sMkt = Market.getInstance() + item = sMkt.getItem(objitem) + + return item.price.price + + def getPrices(self, objitems, callback, waitforthread=False): + """Get prices for multiple typeIDs""" + requests = [] + for objitem in objitems: + sMkt = Market.getInstance() + item = sMkt.getItem(objitem) + requests.append(item.price) + + def cb(): + try: + callback(requests) + except Exception as e: + pyfalog.critical("Callback failed.") + pyfalog.critical(e) + db.commit() + + if waitforthread: + self.priceWorkerThread.setToWait(requests, cb) + else: + self.priceWorkerThread.trigger(requests, cb) + + def clearPriceCache(self): + pyfalog.debug("Clearing Prices") + db.clearPrices() + + +class PriceWorkerThread(threading.Thread): + def __init__(self): + threading.Thread.__init__(self) + self.name = "PriceWorker" + self.queue = Queue.Queue() + self.wait = {} + pyfalog.debug("Initialize PriceWorkerThread.") + + def run(self): + queue = self.queue + while True: + # Grab our data + callback, requests = queue.get() + + # Grab prices, this is the time-consuming part + if len(requests) > 0: + Price.fetchPrices(requests) + + wx.CallAfter(callback) + queue.task_done() + + # After we fetch prices, go through the list of waiting items and call their callbacks + for price in requests: + callbacks = self.wait.pop(price.typeID, None) + if callbacks: + for callback in callbacks: + wx.CallAfter(callback) + + def trigger(self, prices, callbacks): + self.queue.put((callbacks, prices)) + + def setToWait(self, itemID, callback): + if itemID not in self.wait: + self.wait[itemID] = [] + self.wait[itemID].append(callback) diff --git a/service/settings.py b/service/settings.py index cfc48fc4a..0197564c1 100644 --- a/service/settings.py +++ b/service/settings.py @@ -29,7 +29,8 @@ pyfalog = Logger(__name__) class SettingsProvider(object): - BASE_PATH = os.path.join(config.savePath, 'settings') + if config.savePath: + BASE_PATH = os.path.join(config.savePath, 'settings') settings = {} _instance = None @@ -41,38 +42,65 @@ class SettingsProvider(object): return cls._instance def __init__(self): - if not os.path.exists(self.BASE_PATH): - os.mkdir(self.BASE_PATH) + if hasattr(self, 'BASE_PATH'): + if not os.path.exists(self.BASE_PATH): + os.mkdir(self.BASE_PATH) + # def getSettings(self, area, defaults=None): + # # type: (basestring, dict) -> service.Settings + # # NOTE: needed to change for tests + # settings_obj = self.settings.get(area) + # + # if settings_obj is None and hasattr(self, 'BASE_PATH'): + # canonical_path = os.path.join(self.BASE_PATH, area) + # + # if not os.path.exists(canonical_path): + # info = {} + # if defaults: + # for item in defaults: + # info[item] = defaults[item] + # + # else: + # try: + # f = open(canonical_path, "rb") + # info = cPickle.load(f) + # for item in defaults: + # if item not in info: + # info[item] = defaults[item] + # + # except: + # info = {} + # if defaults: + # for item in defaults: + # info[item] = defaults[item] + # + # self.settings[area] = settings_obj = Settings(canonical_path, info) + # + # return settings_obj def getSettings(self, area, defaults=None): - - s = self.settings.get(area) - if s is None: - p = os.path.join(self.BASE_PATH, area) - - if not os.path.exists(p): + # type: (basestring, dict) -> service.Settings + # NOTE: needed to change for tests + # TODO: Write to memory with mmap -> https://docs.python.org/2/library/mmap.html + settings_obj = self.settings.get(area) + if settings_obj is None: # and hasattr(self, 'BASE_PATH'): + canonical_path = os.path.join(self.BASE_PATH, area) if hasattr(self, 'BASE_PATH') else "" + if not os.path.exists(canonical_path): # path string or empty string. info = {} if defaults: - for item in defaults: - info[item] = defaults[item] - + info.update(defaults) else: try: - f = open(p, "rb") - info = cPickle.load(f) + with open(canonical_path, "rb") as f: + info = cPickle.load(f) for item in defaults: if item not in info: info[item] = defaults[item] - except: info = {} - if defaults: - for item in defaults: - info[item] = defaults[item] + info.update(defaults) - self.settings[area] = s = Settings(p, info) - - return s + self.settings[area] = settings_obj = Settings(canonical_path, info) + return settings_obj def saveAll(self): for settings in self.settings.itervalues(): @@ -81,12 +109,22 @@ class SettingsProvider(object): class Settings(object): def __init__(self, location, info): + # type: (basestring, dict) -> None + # path string or empty string. self.location = location self.info = info + # def save(self): + # f = open(self.location, "wb") + # cPickle.dump(self.info, f, cPickle.HIGHEST_PROTOCOL) + def save(self): - f = open(self.location, "wb") - cPickle.dump(self.info, f, cPickle.HIGHEST_PROTOCOL) + # NOTE: needed to change for tests + if self.location is None or not self.location: + return + # NOTE: with + open -> file handle auto close + with open(self.location, "wb") as f: + cPickle.dump(self.info, f, cPickle.HIGHEST_PROTOCOL) def __getitem__(self, k): try: @@ -260,8 +298,7 @@ class HTMLExportSettings(object): def __init__(self): serviceHTMLExportDefaultSettings = { - "enabled": False, - "path" : config.pyfaPath + os.sep + 'pyfaFits.html', + "path" : config.savePath + os.sep + 'pyfaFits.html', "minimal": False } self.serviceHTMLExportSettings = SettingsProvider.getInstance().getSettings( @@ -269,12 +306,6 @@ class HTMLExportSettings(object): serviceHTMLExportDefaultSettings ) - def getEnabled(self): - return self.serviceHTMLExportSettings["enabled"] - - def setEnabled(self, enabled): - self.serviceHTMLExportSettings["enabled"] = enabled - def getMinimalEnabled(self): return self.serviceHTMLExportSettings["minimal"] diff --git a/tests/jeffy_ja-en[99].xml b/tests/jeffy_ja-en[99].xml new file mode 100644 index 000000000..8ec178b16 --- /dev/null +++ b/tests/jeffy_ja-en[99].xml @@ -0,0 +1,2116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_locale/file_dialog.py b/tests/test_locale/file_dialog.py new file mode 100644 index 000000000..6ef38f12a --- /dev/null +++ b/tests/test_locale/file_dialog.py @@ -0,0 +1,93 @@ +# noinspection PyPackageRequirements +import wx +import sys +import os +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +# Add root to python paths, this allows us to import submodules +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..'))) + +from _development.helpers_locale import GetPath, GetUnicodePath + +class MyForm(wx.Frame): + # ---------------------------------------------------------------------- + def __init__(self): + wx.Frame.__init__(self, None, wx.ID_ANY, "CTRL-O to open, CTRL-S to save", size=(500, 500)) + + # Add a panel so it looks the correct on all platforms + panel = wx.Panel(self, wx.ID_ANY) + + SAVE_FILE_ID = wx.NewId() + self.Bind(wx.EVT_MENU, self.saveFile, id=SAVE_FILE_ID) + + LOAD_FILE_ID = wx.NewId() + self.Bind(wx.EVT_MENU, self.loadFile, id=LOAD_FILE_ID) + + accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord('O'), LOAD_FILE_ID), + (wx.ACCEL_CTRL, ord('S'), SAVE_FILE_ID)] + ) + self.SetAcceleratorTable(accel_tbl) + + # ---------------------------------------------------------------------- + def loadFile(self, event): + openFileDialog = wx.FileDialog(self, "Open", "", "", + "Python files (*.py)|*.py", + wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) + openFileDialog.ShowModal() + path = openFileDialog.GetPath() + try: + os_walk_without_codec = GetPath(path) + except (UnicodeEncodeError, UnicodeTranslateError, UnicodeError, UnicodeDecodeError, UnicodeWarning, TypeError) as e: + os_walk_without_codec = e + + try: + os_walk_with_system_codec = GetPath(path, None, sys.getdefaultencoding()) + except (UnicodeEncodeError, UnicodeTranslateError, UnicodeError, UnicodeDecodeError, UnicodeWarning, TypeError) as e: + os_walk_with_system_codec = e + + try: + os_walk_unicode_without_codec = GetUnicodePath(path) + except (UnicodeEncodeError, UnicodeTranslateError, UnicodeError, UnicodeDecodeError, UnicodeWarning, TypeError) as e: + os_walk_unicode_without_codec = e + + try: + os_walk_unicode_with_system_codec = GetUnicodePath(path, None, sys.getdefaultencoding()) + except (UnicodeEncodeError, UnicodeTranslateError, UnicodeError, UnicodeDecodeError, UnicodeWarning, TypeError) as e: + os_walk_unicode_with_system_codec = e + + print("Simple print:") + print(path) + + print("Type:") + print(type(path)) + + print("OS Walk: No Codec:") + print(os_walk_without_codec) + + print("OS Walk: Default System Codec:") + print(os_walk_with_system_codec) + + print("OS Unicode Walk: No Codec:") + print(os_walk_unicode_without_codec) + + print("OS Unicode Walk: Default System Codec:") + print(os_walk_unicode_with_system_codec) + openFileDialog.Destroy() + + # ---------------------------------------------------------------------- + def saveFile(self, event): + saveFileDialog = wx.FileDialog(self, "Save As", "", "", + "Python files (*.py)|*.py", + wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) + saveFileDialog.ShowModal() + saveFileDialog.GetPath() + saveFileDialog.Destroy() + + +# Run the program +if __name__ == "__main__": + app = wx.App(False) + frame = MyForm() + frame.Show() + app.MainLoop() diff --git a/tests/test_locale/readme.md b/tests/test_locale/readme.md new file mode 100644 index 000000000..bf413bae3 --- /dev/null +++ b/tests/test_locale/readme.md @@ -0,0 +1 @@ +Use this to dynamically test languages. \ No newline at end of file diff --git a/tests/test_locale/test_Pyfa/test_codec_english.py b/tests/test_locale/test_Pyfa/test_codec_english.py new file mode 100644 index 000000000..b2f03f69f --- /dev/null +++ b/tests/test_locale/test_Pyfa/test_codec_english.py @@ -0,0 +1,36 @@ +# English + +import os +import platform +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +# Add root to python paths, this allows us to import submodules +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + +from _development.helpers_locale import GetPath + + +def test_codec_english(): + use_codec = { + "Windows": "cp1252", + "Linux" : "utf8", + "Darwin" : "utf8", + } + + os_name = platform.system() + current_directory = os.path.dirname(os.path.abspath(__file__)) + + try: + decoded_file = GetPath(current_directory, "testcodec", use_codec[os_name]) + except: + assert False, "Specified codec (" + use_codec[os_name] + ") failed to decrypt file path." + + try: + with open(decoded_file, 'r') as f: + read_data = f.read() + f.closed + except: + assert False, "Specified codec (" + use_codec[os_name] + ") failed to read file." + + assert read_data == "True" diff --git a/tests/test_locale/test_Pyfa/testcodec b/tests/test_locale/test_Pyfa/testcodec new file mode 100644 index 000000000..4791ed555 --- /dev/null +++ b/tests/test_locale/test_Pyfa/testcodec @@ -0,0 +1 @@ +True \ No newline at end of file diff --git a/tests/test_locale/test_os_walk.py b/tests/test_locale/test_os_walk.py new file mode 100644 index 000000000..4ed377acc --- /dev/null +++ b/tests/test_locale/test_os_walk.py @@ -0,0 +1,40 @@ +import os +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +# Add root to python paths, this allows us to import submodules +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..'))) + +from _development.helpers_locale import GetPath + +def test_os_walk(): + current_directory = os.path.dirname(os.path.abspath(unicode(__file__))) + subfolders = os.listdir(current_directory) + subfolders = [e for e in subfolders if not (e.endswith(".py") or e.endswith(".pyc") or e.endswith(".md"))] + + subfolder_count = 0 + for subfolder in subfolders: + subdir = GetPath(current_directory, subfolder) + testfile = GetPath(subdir, "testcodec") + + if "__pycache__" in testfile: + # Grabbed a Travis temp folder, skip any assertions, but count it. + subfolder_count += 1 + continue + + # noinspection PyBroadException + try: + with open(testfile, 'r') as f: + read_data = f.read() + # noinspection PyStatementEffect + f.closed + except: + print("Test File:") + print(testfile) + assert False, "Failed to read file." + + read_data = read_data.replace("\n", "") + assert read_data == "True" + subfolder_count += 1 + + assert len(subfolders) == subfolder_count diff --git a/tests/test_locale/test_знаф/test_codec_russian.py b/tests/test_locale/test_знаф/test_codec_russian.py new file mode 100644 index 000000000..1572b6bd5 --- /dev/null +++ b/tests/test_locale/test_знаф/test_codec_russian.py @@ -0,0 +1,36 @@ +# Russian + +import os +import platform +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +# Add root to python paths, this allows us to import submodules +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + +from _development.helpers_locale import GetPath + + +def test_codec_russian(): + use_codec = { + "Windows": "cp1251", + "Linux" : "utf8", + "Darwin" : "mac_cyrillic", + } + + os_name = platform.system() + current_directory = os.path.dirname(os.path.abspath(__file__)) + + try: + decoded_file = GetPath(current_directory, "testcodec", use_codec[os_name]) + except: + assert False, "Specified codec (" + use_codec[os_name] + ") failed to decrypt file path." + + try: + with open(decoded_file, 'r') as f: + read_data = f.read() + f.closed + except: + assert False, "Specified codec (" + use_codec[os_name] + ") failed to read file." + + assert read_data == "True" diff --git a/tests/test_locale/test_знаф/testcodec b/tests/test_locale/test_знаф/testcodec new file mode 100644 index 000000000..4791ed555 --- /dev/null +++ b/tests/test_locale/test_знаф/testcodec @@ -0,0 +1 @@ +True \ No newline at end of file diff --git a/tests/test_locale/test_פטכש/test_codec_hebrew.py b/tests/test_locale/test_פטכש/test_codec_hebrew.py new file mode 100644 index 000000000..6b806cbe8 --- /dev/null +++ b/tests/test_locale/test_פטכש/test_codec_hebrew.py @@ -0,0 +1,36 @@ +# Hebrew + +import os +import platform +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +# Add root to python paths, this allows us to import submodules +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + +from _development.helpers_locale import GetPath + + +def test_codec_hebrew(): + use_codec = { + "Windows": "cp1252", + "Linux" : "utf8", + "Darwin" : "utf8", + } + + os_name = platform.system() + current_directory = os.path.dirname(os.path.abspath(__file__)) + + try: + decoded_file = GetPath(current_directory, "testcodec", use_codec[os_name]) + except: + assert False, "Specified codec (" + use_codec[os_name] + ") failed to decrypt file path." + + try: + with open(decoded_file, 'r') as f: + read_data = f.read() + f.closed + except: + assert False, "Specified codec (" + use_codec[os_name] + ") failed to read file." + + assert read_data == "True" diff --git a/tests/test_locale/test_פטכש/testcodec b/tests/test_locale/test_פטכש/testcodec new file mode 100644 index 000000000..4791ed555 --- /dev/null +++ b/tests/test_locale/test_פטכש/testcodec @@ -0,0 +1 @@ +True \ No newline at end of file diff --git a/tests/test_locale/test_测试/test_codec_chinese_simplified.py b/tests/test_locale/test_测试/test_codec_chinese_simplified.py new file mode 100644 index 000000000..30d879391 --- /dev/null +++ b/tests/test_locale/test_测试/test_codec_chinese_simplified.py @@ -0,0 +1,36 @@ +# Chinese (Simplified) + +import os +import platform +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +# Add root to python paths, this allows us to import submodules +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + +from _development.helpers_locale import GetPath + + +def test_codec_chinese_simplified(): + use_codec = { + "Windows": "cp1252", + "Linux" : "utf8", + "Darwin" : "utf8", + } + + os_name = platform.system() + current_directory = os.path.dirname(os.path.abspath(__file__)) + + try: + decoded_file = GetPath(current_directory, "testcodec", use_codec[os_name]) + except: + assert False, "Specified codec (" + use_codec[os_name] + ") failed to decrypt file path." + + try: + with open(decoded_file, 'r') as f: + read_data = f.read() + f.closed + except: + assert False, "Specified codec (" + use_codec[os_name] + ") failed to read file." + + assert read_data == "True" diff --git a/tests/test_locale/test_测试/testcodec b/tests/test_locale/test_测试/testcodec new file mode 100644 index 000000000..4791ed555 --- /dev/null +++ b/tests/test_locale/test_测试/testcodec @@ -0,0 +1 @@ +True \ No newline at end of file diff --git a/tests/test_modules/gui/test_aboutData.py b/tests/test_modules/gui/test_aboutData.py deleted file mode 100644 index 8e7e862d6..000000000 --- a/tests/test_modules/gui/test_aboutData.py +++ /dev/null @@ -1,9 +0,0 @@ -from gui.aboutData import versionString, licenses, developers, credits, description - - -def test_aboutData(): - assert versionString.__len__() > 0 - assert licenses.__len__() > 0 - assert developers.__len__() > 0 - assert credits.__len__() > 0 - assert description.__len__() > 0 diff --git a/tests/test_modules/test_eos/test_gamedata.py b/tests/test_modules/test_eos/test_gamedata.py new file mode 100644 index 000000000..4a8674339 --- /dev/null +++ b/tests/test_modules/test_eos/test_gamedata.py @@ -0,0 +1,17 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +from _development.helpers_fits import RifterFit, KeepstarFit + +def test_race(DB, RifterFit, KeepstarFit): + """ + Test race code + """ + assert RifterFit.ship.item.race == 'minmatar' + assert KeepstarFit.ship.item.race == 'upwell' diff --git a/tests/test_modules/test_eos/test_modifiedAttributeDict.py b/tests/test_modules/test_eos/test_modifiedAttributeDict.py new file mode 100644 index 000000000..55b68fb14 --- /dev/null +++ b/tests/test_modules/test_eos/test_modifiedAttributeDict.py @@ -0,0 +1,50 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import math +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +script_dir = os.path.realpath(os.path.join(script_dir, '..', '..', '..')) +print script_dir +sys.path.append(script_dir) + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +from _development.helpers_fits import RifterFit + +def test_multiply_stacking_penalties(DB, Saveddata, RifterFit): + """ + Tests the stacking penalties under multiply + """ + char0 = Saveddata['Character'].getAll0() + + RifterFit.character = char0 + starting_em_resist = RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") + + mod = Saveddata['Module'](DB['db'].getItem("EM Ward Amplifier II")) + item_modifer = mod.item.getAttribute("emDamageResistanceBonus") + + RifterFit.calculateModifiedAttributes() + + for _ in range(10): + if _ == 0: + # First run we have no modules, se don't try and calculate them. + calculated_resist = RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") + else: + # Calculate what our next resist should be + # Denominator: [math.exp((i / 2.67) ** 2.0) for i in xrange(8)] + current_effectiveness = 1 / math.exp(((_ - 1) / 2.67) ** 2.0) + new_item_modifier = 1 + ((item_modifer * current_effectiveness) / 100) + calculated_resist = (em_resist * new_item_modifier) + + # Add another resist module to our fit. + RifterFit.modules.append(mod) + + # Modify our fit so that Eos generates new numbers for us. + RifterFit.clear() + RifterFit.calculateModifiedAttributes() + + em_resist = RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") + + assert em_resist == calculated_resist + # print(str(em_resist) + "==" + str(calculated_resist)) diff --git a/tests/test_modules/test_eos/test_saveddata/test_booster.py b/tests/test_modules/test_eos/test_saveddata/test_booster.py new file mode 100644 index 000000000..05576c3fc --- /dev/null +++ b/tests/test_modules/test_eos/test_saveddata/test_booster.py @@ -0,0 +1,36 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..', '..'))) + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +from _development.helpers_fits import RifterFit, KeepstarFit +from _development.helpers_items import StrongBluePillBooster + + +def test_itemModifiedAttributes(DB, StrongBluePillBooster): + assert StrongBluePillBooster.itemModifiedAttributes is not None + + +def test_isInvalid(DB, StrongBluePillBooster): + assert StrongBluePillBooster.isInvalid is False + + +def test_slot(DB, StrongBluePillBooster): + assert StrongBluePillBooster.slot == 1 + + +def test_item(DB, Gamedata, StrongBluePillBooster): + assert isinstance(StrongBluePillBooster.item, Gamedata['Item']) + + +def test_clear(DB, StrongBluePillBooster): + try: + StrongBluePillBooster.clear() + assert True + except: + assert False diff --git a/tests/test_modules/test_eos/test_saveddata/test_fit_2.py b/tests/test_modules/test_eos/test_saveddata/test_fit_2.py new file mode 100644 index 000000000..5f2bdfb0b --- /dev/null +++ b/tests/test_modules/test_eos/test_saveddata/test_fit_2.py @@ -0,0 +1,125 @@ +# TODO: Drop the `_2` from the file name once one of our fit files are renamed + +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +from copy import deepcopy + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..', '..'))) + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +from _development.helpers_fits import RifterFit, KeepstarFit, HeronFit + + +def test_calculateModifiedAttributes(DB, RifterFit, KeepstarFit): + rifter_modifier_dicts = { + '_ModifiedAttributeDict__affectedBy' : 26, + '_ModifiedAttributeDict__forced' : 0, + '_ModifiedAttributeDict__intermediary' : 0, + '_ModifiedAttributeDict__modified' : 26, + '_ModifiedAttributeDict__multipliers' : 22, + '_ModifiedAttributeDict__overrides' : 0, + '_ModifiedAttributeDict__penalizedMultipliers': 0, + '_ModifiedAttributeDict__postIncreases' : 0, + '_ModifiedAttributeDict__preAssigns' : 0, + '_ModifiedAttributeDict__preIncreases' : 4, + } + + # Test before calculating attributes + for test_dict in rifter_modifier_dicts: + assert len(getattr(RifterFit.ship.itemModifiedAttributes, test_dict)) == 0 + + RifterFit.calculateModifiedAttributes() + + for test_dict in rifter_modifier_dicts: + assert len(getattr(RifterFit.ship.itemModifiedAttributes, test_dict)) == rifter_modifier_dicts[test_dict] + + # Keepstars don't have any basic skills that would change their attributes + keepstar_modifier_dicts = { + '_ModifiedAttributeDict__affectedBy' : 0, + '_ModifiedAttributeDict__forced' : 0, + '_ModifiedAttributeDict__intermediary' : 0, + '_ModifiedAttributeDict__modified' : 0, + '_ModifiedAttributeDict__multipliers' : 0, + '_ModifiedAttributeDict__overrides' : 0, + '_ModifiedAttributeDict__penalizedMultipliers': 0, + '_ModifiedAttributeDict__postIncreases' : 0, + '_ModifiedAttributeDict__preAssigns' : 0, + '_ModifiedAttributeDict__preIncreases' : 0, + } + + # Test before calculating attributes + for test_dict in keepstar_modifier_dicts: + assert len(getattr(KeepstarFit.ship.itemModifiedAttributes, test_dict)) == 0 + + KeepstarFit.calculateModifiedAttributes() + + for test_dict in keepstar_modifier_dicts: + assert len(getattr(KeepstarFit.ship.itemModifiedAttributes, test_dict)) == keepstar_modifier_dicts[test_dict] + +def test_calculateModifiedAttributes_withProjected(DB, RifterFit, HeronFit): + # TODO: This test is not currently functional or meaningful as projections are not happening correctly. + # This is true for all tested branches (master, dev, etc) + rifter_modifier_dicts = { + '_ModifiedAttributeDict__affectedBy' : 26, + '_ModifiedAttributeDict__forced' : 0, + '_ModifiedAttributeDict__intermediary' : 0, + '_ModifiedAttributeDict__modified' : 26, + '_ModifiedAttributeDict__multipliers' : 22, + '_ModifiedAttributeDict__overrides' : 0, + '_ModifiedAttributeDict__penalizedMultipliers': 0, + '_ModifiedAttributeDict__postIncreases' : 0, + '_ModifiedAttributeDict__preAssigns' : 0, + '_ModifiedAttributeDict__preIncreases' : 4, + } + + # Test before calculating attributes + for test_dict in rifter_modifier_dicts: + assert len(getattr(RifterFit.ship.itemModifiedAttributes, test_dict)) == 0 + + # Get base stats + max_target_range_1 = RifterFit.ship.getModifiedItemAttr('maxTargetRange') + scan_resolution_1 = RifterFit.ship.getModifiedItemAttr('scanResolution') + + RifterFit.clear() + RifterFit.calculateModifiedAttributes() + + # Get self calculated stats + max_target_range_2 = RifterFit.ship.getModifiedItemAttr('maxTargetRange') + scan_resolution_2 = RifterFit.ship.getModifiedItemAttr('scanResolution') + + RifterFit.clear() + # Project Heron fit onto Rifter + RifterFit._Fit__projectedFits[HeronFit.ID] = HeronFit + + # DB['saveddata_session'].commit() + # DB['saveddata_session'].flush() + # DB['saveddata_session'].refresh(HeronFit) + + RifterFit.calculateModifiedAttributes() + + # Get stats with projections + max_target_range_3 = RifterFit.ship.getModifiedItemAttr('maxTargetRange') + scan_resolution_3 = RifterFit.ship.getModifiedItemAttr('scanResolution') + + RifterFit.clear() + RifterFit.calculateModifiedAttributes() + + # Get stats with projections + max_target_range_4 = RifterFit.ship.getModifiedItemAttr('maxTargetRange') + scan_resolution_4 = RifterFit.ship.getModifiedItemAttr('scanResolution') + + RifterFit.clear() + HeronFit.calculateModifiedAttributes(targetFit=RifterFit) + RifterFit.calculateModifiedAttributes() + + # Get stats with projections + max_target_range_5 = RifterFit.ship.getModifiedItemAttr('maxTargetRange') + scan_resolution_5 = RifterFit.ship.getModifiedItemAttr('scanResolution') + + for test_dict in rifter_modifier_dicts: + assert len(getattr(RifterFit.ship.itemModifiedAttributes, test_dict)) == rifter_modifier_dicts[test_dict] + diff --git a/tests/test_modules/test_gui/test_aboutData.py b/tests/test_modules/test_gui/test_aboutData.py new file mode 100644 index 000000000..b17668e68 --- /dev/null +++ b/tests/test_modules/test_gui/test_aboutData.py @@ -0,0 +1,19 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + +from gui.aboutData import versionString, licenses, developers, credits, description + + +def test_aboutData(): + """ + Simple test to validate all about data exists + """ + assert versionString.__len__() > 0 + assert licenses.__len__() > 0 + assert developers.__len__() > 0 + assert credits.__len__() > 0 + assert description.__len__() > 0 diff --git a/tests/test_modules/service/test_attribute.py b/tests/test_modules/test_service/test_attribute.py similarity index 82% rename from tests/test_modules/service/test_attribute.py rename to tests/test_modules/test_service/test_attribute.py index 1ab44f2a8..0f9622885 100644 --- a/tests/test_modules/service/test_attribute.py +++ b/tests/test_modules/test_service/test_attribute.py @@ -1,9 +1,16 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + from service.attribute import Attribute def test_attribute(): """ - We don't really have much to test here, to throw a generic attribute at it and validate we get the expected results + We don't really have much to test here, so throw a generic attribute at it and validate we get the expected results :return: """ diff --git a/tests/test_modules/test_service/test_fit.py b/tests/test_modules/test_service/test_fit.py new file mode 100644 index 000000000..b110dd2da --- /dev/null +++ b/tests/test_modules/test_service/test_fit.py @@ -0,0 +1,34 @@ +# Add root folder to python paths +import os +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..', '..'))) + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +# noinspection PyPackageRequirements +from _development.helpers_fits import RifterFit, KeepstarFit +from service.fit import Fit + + +def test_getAllFits(DB, RifterFit, KeepstarFit): + assert len(Fit.getAllFits()) == 0 + + DB['db'].save(RifterFit) + DB['db'].save(KeepstarFit) + + # For some reason in Travis this adds the first fit twice. WHY?!? + assert len(Fit.getAllFits()) != 0 + + # Cleanup after ourselves + DB['db'].remove(RifterFit) + DB['db'].remove(KeepstarFit) + + +def test_getFitsWithShip_RifterFit(DB, RifterFit): + DB['db'].save(RifterFit) + + assert Fit.getFitsWithShip(587)[0][1] == 'My Rifter Fit' + + DB['db'].remove(RifterFit) diff --git a/tests/test_package.py b/tests/test_package.py deleted file mode 100644 index 5f97967d7..000000000 --- a/tests/test_package.py +++ /dev/null @@ -1,58 +0,0 @@ -"""import tests.""" - -import os -import sys -# import importlib - -# noinspection PyPackageRequirements -# import pytest - - -script_dir = os.path.dirname(os.path.abspath(__file__)) -# Add root to python paths, this allows us to import submodules -sys.path.append(os.path.realpath(os.path.join(script_dir, '..'))) - -# noinspection PyPep8 -import service -# noinspection PyPep8 -import gui -# noinspection PyPep8 -import eos -# noinspection PyPep8 -import utils - - -def test_packages(): - assert service - assert gui - assert eos - assert utils - - -def service_modules(): - for root, folders, files in os.walk("service"): - for file_ in files: - if file_.endswith(".py") and not file_.startswith("_"): - mod_name = "{}.{}".format( - root.replace("/", "."), - file_.split(".py")[0], - ) - yield mod_name - - -def eos_modules(): - for root, folders, files in os.walk("eos"): - for file_ in files: - if file_.endswith(".py") and not file_.startswith("_"): - mod_name = "{}.{}".format( - root.replace("/", "."), - file_.split(".py")[0], - ) - yield mod_name - -# TODO: Disable walk through Eos paths until eos.types is killed. eos.types causes the import to break -''' -@pytest.mark.parametrize("mod_name", eos_modules()) -def test_eos_imports(mod_name): - assert importlib.import_module(mod_name) -''' diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py new file mode 100644 index 000000000..e81b0d2e1 --- /dev/null +++ b/tests/test_placeholder.py @@ -0,0 +1,4 @@ +# This test does nothing. It just lets us right click and run all tests straight from the `tests` folder. + +def test_nothing(): + assert True diff --git a/tests/test_smoketests/test_rifter.py b/tests/test_smoketests/test_rifter.py new file mode 100644 index 000000000..b6bb63ca5 --- /dev/null +++ b/tests/test_smoketests/test_rifter.py @@ -0,0 +1,235 @@ +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..', '..'))) + +# noinspection PyPackageRequirements +from _development.helpers import DBInMemory as DB, Gamedata, Saveddata +from _development.helpers_fits import RifterFit + + +# noinspection PyShadowingNames +def test_rifter_empty_char0(DB, Saveddata, RifterFit): + """ + We test an empty ship because if we use this as a base for testing our V skills, + and CCP ever fucks with the base states, all our derived stats will be wrong. + """ + char0 = Saveddata['Character'].getAll0() + + RifterFit.character = char0 + RifterFit.calculateModifiedAttributes() + + assert RifterFit.ship.getModifiedItemAttr("agility") == 3.2 + assert RifterFit.ship.getModifiedItemAttr("armorEmDamageResonance") == 0.4 + assert RifterFit.ship.getModifiedItemAttr("armorExplosiveDamageResonance") == 0.9 + assert RifterFit.ship.getModifiedItemAttr("armorHP") == 450.0 + assert RifterFit.ship.getModifiedItemAttr("armorKineticDamageResonance") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("armorThermalDamageResonance") == 0.65 + assert RifterFit.ship.getModifiedItemAttr("armorUniformity") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("baseWarpSpeed") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("capacitorCapacity") == 250.0 + assert RifterFit.ship.getModifiedItemAttr("capacity") == 140.0 + assert RifterFit.ship.getModifiedItemAttr("cpuLoad") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 130.0 + assert RifterFit.ship.getModifiedItemAttr("damage") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("droneBandwidth") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("droneCapacity") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("emDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("explosiveDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("fwLpKill") == 25.0 + assert RifterFit.ship.getModifiedItemAttr("gfxBoosterID") == 397.0 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationHi") == 0.63 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationLow") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationMed") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityHi") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityLow") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityMed") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateHi") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateLow") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateMed") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatGenerationMultiplier") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hiSlots") == 4.0 + assert RifterFit.ship.getModifiedItemAttr("hp") == 350.0 + assert RifterFit.ship.getModifiedItemAttr("hullEmDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullExplosiveDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullKineticDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullThermalDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("kineticDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("launcherSlotsLeft") == 2.0 + assert RifterFit.ship.getModifiedItemAttr("lowSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("mainColor") == 16777215.0 + assert RifterFit.ship.getModifiedItemAttr("mass") == 1067000.0 + assert RifterFit.ship.getModifiedItemAttr("maxDirectionalVelocity") == 3000.0 + assert RifterFit.ship.getModifiedItemAttr("maxLockedTargets") == 4.0 + assert RifterFit.ship.getModifiedItemAttr("maxPassengers") == 2.0 + assert RifterFit.ship.getModifiedItemAttr("maxTargetRange") == 22500.0 + assert RifterFit.ship.getModifiedItemAttr("maxVelocity") == 365.0 + assert RifterFit.ship.getModifiedItemAttr("medSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("metaLevel") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("minTargetVelDmgMultiplier") == 0.05 + assert RifterFit.ship.getModifiedItemAttr("powerLoad") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("powerOutput") == 41.0 + assert RifterFit.ship.getModifiedItemAttr("powerToSpeed") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("propulsionGraphicID") == 397.0 + assert RifterFit.ship.getModifiedItemAttr("radius") == 31.0 + assert RifterFit.ship.getModifiedItemAttr("rechargeRate") == 125000.0 + assert RifterFit.ship.getModifiedItemAttr("requiredSkill1") == 3329.0 + assert RifterFit.ship.getModifiedItemAttr("requiredSkill1Level") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("rigSize") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("rigSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("scanGravimetricStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanLadarStrength") == 8.0 + assert RifterFit.ship.getModifiedItemAttr("scanMagnetometricStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanRadarStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanResolution") == 660.0 + assert RifterFit.ship.getModifiedItemAttr("scanSpeed") == 1500.0 + assert RifterFit.ship.getModifiedItemAttr("shieldCapacity") == 450.0 + assert RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("shieldExplosiveDamageResonance") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("shieldKineticDamageResonance") == 0.6 + assert RifterFit.ship.getModifiedItemAttr("shieldRechargeRate") == 625000.0 + assert RifterFit.ship.getModifiedItemAttr("shieldThermalDamageResonance") == 0.8 + assert RifterFit.ship.getModifiedItemAttr("shieldUniformity") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("shipBonusMF") == 5.0 + assert RifterFit.ship.getModifiedItemAttr("shipBonusMF2") == 10.0 + assert RifterFit.ship.getModifiedItemAttr("shipScanResistance") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("signatureRadius") == 35.0 + assert RifterFit.ship.getModifiedItemAttr("structureUniformity") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("techLevel") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("thermalDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("turretSlotsLeft") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("typeColorScheme") == 11342.0 + assert RifterFit.ship.getModifiedItemAttr("uniformity") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("upgradeCapacity") == 400.0 + assert RifterFit.ship.getModifiedItemAttr("upgradeSlotsLeft") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("volume") == 27289.0 + assert RifterFit.ship.getModifiedItemAttr("warpCapacitorNeed") == 2.24e-06 + assert RifterFit.ship.getModifiedItemAttr("warpFactor") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("warpSpeedMultiplier") == 5.0 + + +# noinspection PyShadowingNames +def test_rifter_empty_char5(DB, Saveddata, RifterFit): + """ + Test char skills applying to a ship + """ + char5 = Saveddata['Character'].getAll5() + + RifterFit.character = char5 + RifterFit.calculateModifiedAttributes() + + assert RifterFit.ship.getModifiedItemAttr("agility") == 2.16 + assert RifterFit.ship.getModifiedItemAttr("armorEmDamageResonance") == 0.4 + assert RifterFit.ship.getModifiedItemAttr("armorExplosiveDamageResonance") == 0.9 + assert RifterFit.ship.getModifiedItemAttr("armorHP") == 562.5 + assert RifterFit.ship.getModifiedItemAttr("armorKineticDamageResonance") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("armorThermalDamageResonance") == 0.65 + assert RifterFit.ship.getModifiedItemAttr("armorUniformity") == 0.75 + assert RifterFit.ship.getModifiedItemAttr("baseWarpSpeed") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("capacitorCapacity") == 312.5 + assert RifterFit.ship.getModifiedItemAttr("capacity") == 140.0 + assert RifterFit.ship.getModifiedItemAttr("cpuLoad") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 162.5 + assert RifterFit.ship.getModifiedItemAttr("damage") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("droneBandwidth") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("droneCapacity") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("emDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("explosiveDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("fwLpKill") == 25.0 + assert RifterFit.ship.getModifiedItemAttr("gfxBoosterID") == 397.0 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationHi") == 0.63 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationLow") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("heatAttenuationMed") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityHi") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityLow") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatCapacityMed") == 100.0 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateHi") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateLow") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatDissipationRateMed") == 0.01 + assert RifterFit.ship.getModifiedItemAttr("heatGenerationMultiplier") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hiSlots") == 4.0 + assert RifterFit.ship.getModifiedItemAttr("hp") == 437.5 + assert RifterFit.ship.getModifiedItemAttr("hullEmDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullExplosiveDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullKineticDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("hullThermalDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("kineticDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("launcherSlotsLeft") == 2.0 + assert RifterFit.ship.getModifiedItemAttr("lowSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("mainColor") == 16777215.0 + assert RifterFit.ship.getModifiedItemAttr("mass") == 1067000.0 + assert RifterFit.ship.getModifiedItemAttr("maxDirectionalVelocity") == 3000.0 + assert RifterFit.ship.getModifiedItemAttr("maxLockedTargets") == 4.0 + assert RifterFit.ship.getModifiedItemAttr("maxPassengers") == 2.0 + assert RifterFit.ship.getModifiedItemAttr("maxTargetRange") == 28125.0 + assert RifterFit.ship.getModifiedItemAttr("maxVelocity") == 456.25 + assert RifterFit.ship.getModifiedItemAttr("medSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("metaLevel") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("minTargetVelDmgMultiplier") == 0.05 + assert RifterFit.ship.getModifiedItemAttr("powerLoad") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("powerOutput") == 51.25 + assert RifterFit.ship.getModifiedItemAttr("powerToSpeed") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("propulsionGraphicID") == 397.0 + assert RifterFit.ship.getModifiedItemAttr("radius") == 31.0 + assert RifterFit.ship.getModifiedItemAttr("rechargeRate") == 93750.0 + assert RifterFit.ship.getModifiedItemAttr("requiredSkill1") == 3329.0 + assert RifterFit.ship.getModifiedItemAttr("requiredSkill1Level") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("rigSize") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("rigSlots") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("scanGravimetricStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanLadarStrength") == 9.6 + assert RifterFit.ship.getModifiedItemAttr("scanMagnetometricStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanRadarStrength") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("scanResolution") == 825.0 + assert RifterFit.ship.getModifiedItemAttr("scanSpeed") == 1500.0 + assert RifterFit.ship.getModifiedItemAttr("shieldCapacity") == 562.5 + assert RifterFit.ship.getModifiedItemAttr("shieldEmDamageResonance") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("shieldExplosiveDamageResonance") == 0.5 + assert RifterFit.ship.getModifiedItemAttr("shieldKineticDamageResonance") == 0.6 + assert RifterFit.ship.getModifiedItemAttr("shieldRechargeRate") == 468750.0 + assert RifterFit.ship.getModifiedItemAttr("shieldThermalDamageResonance") == 0.8 + assert RifterFit.ship.getModifiedItemAttr("shieldUniformity") == 1 + assert RifterFit.ship.getModifiedItemAttr("shipBonusMF") == 5.0 + assert RifterFit.ship.getModifiedItemAttr("shipBonusMF2") == 10.0 + assert RifterFit.ship.getModifiedItemAttr("shipScanResistance") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("signatureRadius") == 35.0 + assert RifterFit.ship.getModifiedItemAttr("structureUniformity") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("techLevel") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("thermalDamageResonance") == 0.67 + assert RifterFit.ship.getModifiedItemAttr("turretSlotsLeft") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("typeColorScheme") == 11342.0 + assert RifterFit.ship.getModifiedItemAttr("uniformity") == 1.0 + assert RifterFit.ship.getModifiedItemAttr("upgradeCapacity") == 400.0 + assert RifterFit.ship.getModifiedItemAttr("upgradeSlotsLeft") == 3.0 + assert RifterFit.ship.getModifiedItemAttr("volume") == 27289.0 + assert RifterFit.ship.getModifiedItemAttr("warpCapacitorNeed") == 1.12e-06 + assert RifterFit.ship.getModifiedItemAttr("warpFactor") == 0.0 + assert RifterFit.ship.getModifiedItemAttr("warpSpeedMultiplier") == 5.0 + + +# noinspection PyShadowingNames +def test_rifter_coprocessor(DB, Saveddata, RifterFit): + char5 = Saveddata['Character'].getAll5() + char0 = Saveddata['Character'].getAll0() + + RifterFit.character = char0 + mod = Saveddata['Module'](DB['db'].getItem("Co-Processor II")) + mod.state = Saveddata['State'].OFFLINE + RifterFit.modules.append(mod) + + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 130 + + RifterFit.calculateModifiedAttributes() + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 130 + + mod.state = Saveddata['State'].ONLINE + RifterFit.clear() + RifterFit.calculateModifiedAttributes() + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 143 + + RifterFit.character = char5 + RifterFit.clear() + RifterFit.calculateModifiedAttributes() + assert RifterFit.ship.getModifiedItemAttr("cpuOutput") == 178.75 diff --git a/tests/test_unread_desc.py b/tests/test_unread_desc.py new file mode 100644 index 000000000..0f1a0d52b --- /dev/null +++ b/tests/test_unread_desc.py @@ -0,0 +1,88 @@ +""" + 2017/04/05: unread description tests module. +""" +# noinspection PyPackageRequirements +import pytest +# Add root folder to python paths +# This must be done on every test in order to pass in Travis +import os +import sys +# nopep8 +import re +# from utils.strfunctions import sequential_rep, replace_ltgt +#from utils.stopwatch import Stopwatch + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.realpath(os.path.join(script_dir, '..'))) +sys._called_from_test = True # need db open for tests. (see eos/config.py#17 +# noinspection PyPep8 +from service.port import Port, IPortUser +# +# noinspection PyPackageRequirements +# from _development.helpers import DBInMemory as DB + +""" +NOTE: + description character length is restricted 4hundred by EVE client. + these things apply to multi byte environment too. + + + o read xml fit data (and encode to utf-8 if need. + + o construct xml dom object, and extract "fitting" elements. + + o apply _resolve_ship method to each "fitting" elements. (time measurement + + o extract "hardware" elements from "fitting" element. + + o apply _resolve_module method to each "hardware" elements. (time measurement + +xml files: + "jeffy_ja-en[99].xml" + +NOTE of @decorator: + o Function to receive arguments of function to be decorated + o A function that accepts the decorate target function itself as an argument + o A function that accepts arguments of the decorator itself + +for local coverage: + py.test --cov=./ --cov-report=html +""" + +class PortUser(IPortUser): + + def on_port_processing(self, action, data=None): + print(data) + return True + + +#stpw = Stopwatch('test measurementer') + +@pytest.fixture() +def print_db_info(): + # Output debug info + import eos + print + print "------------ data base connection info ------------" + print(eos.db.saveddata_engine) + print(eos.db.gamedata_engine) + print + + +# noinspection PyUnusedLocal +def test_import_xml(print_db_info): + usr = PortUser() +# for path in XML_FILES: + xml_file = "jeffy_ja-en[99].xml" + fit_count = int(re.search(r"\[(\d+)\]", xml_file).group(1)) + fits = None + with open(os.path.join(script_dir, xml_file), "r") as file_: + srcString = file_.read() + srcString = unicode(srcString, "utf-8") + # (basestring, IPortUser, basestring) -> list[eos.saveddata.fit.Fit] + usr.on_port_process_start() + #stpw.reset() + #with stpw: + fits = Port.importXml(srcString, usr) + + assert fits is not None and len(fits) is fit_count diff --git a/tox.ini b/tox.ini index b92d0b45b..2d1072d50 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pep8 +envlist = py27, pep8 skipsdist = True [testenv] @@ -13,4 +13,4 @@ commands = py.test -vv --cov Pyfa tests/ [testenv:pep8] deps = flake8 # TODO: Remove E731 and convert lambdas to defs -commands = flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py --ignore=E126,E127,E128,E203,E731 service gui eos utils config.py pyfa.py --max-line-length=165 +commands = flake8 --exclude=.svn,CVS,.bzr,.hg,.git,__pycache__,venv,tests,.tox,build,dist,__init__.py,floatspin.py --ignore=E121,E126,E127,E128,E203,E731 service gui eos utils config.py pyfa.py --max-line-length=165 diff --git a/utils/stopwatch.py b/utils/stopwatch.py new file mode 100644 index 000000000..adf7e5832 --- /dev/null +++ b/utils/stopwatch.py @@ -0,0 +1,122 @@ +# coding: utf-8 + +import time +import os + + +class Stopwatch(object): + """ + --- on python console --- +import re +from utils.stopwatch import Stopwatch + +# measurementor +stpw = Stopwatch("test") +# measurement re.sub +def m_re_sub(t, set_count, executes, texts): + t.reset() + while set_count: + set_count -= 1 + with t: + while executes: + executes -= 1 + ret = re.sub("[a|s]+", "-", texts) + # stat string + return str(t) + +# statistics loop: 1000(exec re.sub: 100000) +m_re_sub(stpw, 1000, 100000, "asdfadsasdaasdfadsasda") + +----------- records ----------- + text: "asdfadsasda" + 'elapsed record(ms): min=0.000602411446948, max=220.85578571' + 'elapsed record(ms): min=0.000602411446948, max=217.331377504' + + text: "asdfadsasdaasdfadsasda" + 'elapsed record(ms): min=0.000602411446948, max=287.784902967' + 'elapsed record(ms): min=0.000602411432737, max=283.653264016' + + NOTE: about max + The value is large only at the first execution, + Will it be optimized, after that it will be significantly smaller + """ + + # time.clock() is μs? 1/1000ms + # https://docs.python.jp/2.7/library/time.html#time.clock + _tfunc = time.clock if os.name == "nt" else time.time + + def __init__(self, name='', logger=None): + self.name = name + self.start = Stopwatch._tfunc() + self.__last = self.start + # __last field is means last checkpoint system clock value? + self.logger = logger + self.min = 0.0 + self.max = 0.0 + self.__first = True + + @property + def stat(self): + # :return: (float, float) + return self.min, self.max + + @property + def elapsed(self): + # :return: time as ms + return (Stopwatch._tfunc() - self.start) * 1000 + + @property + def last(self): + return self.__last * 1000 + + def __update_stat(self, v): + # :param v: float unit of ms + if self.__first: + self.__first = False + return + if self.min == 0.0 or self.min > v: + self.min = v + if self.max < v: + self.max = v + + def checkpoint(self, name=''): + span = self.elapsed + self.__update_stat(span) + text = u'Stopwatch("{tname}") - {checkpoint} - {last:.6f}ms ({elapsed:.12f}ms elapsed)'.format( + tname=self.name, + checkpoint=unicode(name, "utf-8"), + last=self.last, + elapsed=span + ).strip() + self.__last = Stopwatch._tfunc() + if self.logger: + self.logger.debug(text) + else: + print(text) + + @staticmethod + def CpuClock(): + start = Stopwatch._tfunc() + time.sleep(1) + return Stopwatch._tfunc() - start + + def reset(self): + # clear stat + self.min = 0.0 + self.max = 0.0 + self.__first = True + + def __enter__(self): + self.start = Stopwatch._tfunc() + return self + + def __exit__(self, type_, value, traceback): + # https://docs.python.org/2.7/reference/datamodel.html?highlight=__enter__#object.__exit__ + # If the context was exited without an exception, all three arguments will be None + self.checkpoint('finished') + # ex: "type=None, value=None, traceback=None" + # print "type=%s, value=%s, traceback=%s" % (type, value, traceback) + return True + + def __repr__(self): + return "elapsed record(ms): min=%s, max=%s" % self.stat diff --git a/utils/strfunctions.py b/utils/strfunctions.py new file mode 100644 index 000000000..26bf5f59a --- /dev/null +++ b/utils/strfunctions.py @@ -0,0 +1,30 @@ +''' + string manipulation module +''' +import re + + +def sequential_rep(text_, *args): + # type: (basestring, tuple) -> basestring + """ + :param text_: string content + :param args: like , , , , ... + :return: if text_ length was zero or invalid parameters then no manipulation to text_ + """ + arg_len = len(args) + if arg_len % 2 == 0 and isinstance(text_, basestring) and len(text_) > 0: + i = 0 + while i < arg_len: + text_ = re.sub(args[i], args[i + 1], text_) + i += 2 + + return text_ + + +def replace_ltgt(text_): + # type: (basestring) -> basestring + """if fit name contained "<" or ">" then reprace to named html entity by EVE client. + :param text_: string content of fit name from exported by EVE client. + :return: if text_ is not instance of basestring then no manipulation to text_. + """ + return text_.replace("<", "<").replace(">", ">") if isinstance(text_, basestring) else text_