Compare commits

..

117 Commits

Author SHA1 Message Date
blitzmann
63fce4be17 Handle self projections by creating a copy of the fit. Due to the way effects are calculated, we would have double effects implemented if not for the copy 2015-07-11 16:28:09 -04:00
blitzmann
86ee5292d8 Fix fit copying and deleting fits not being reflected in other fits. 2015-07-11 12:43:47 -04:00
blitzmann
23b458534f Remove unneeded collection class for projected fits 2015-07-10 23:22:58 -04:00
blitzmann
2256efacb0 Do migration stuff for projected fits 2015-07-10 16:40:00 -04:00
blitzmann
28a5318e3b Merge branch 'master' into toggleProjectionFit
Conflicts:
	config.py
2015-07-10 16:09:00 -04:00
blitzmann
609ee13cd6 Redirect stderr and stdout to logger when we are frozen. Need to test this. 2015-07-10 15:58:45 -04:00
blitzmann
4216904736 Remove function to remove projected fits correctly. This is now handled by proper DB relationships 2015-07-10 15:46:42 -04:00
blitzmann
496e9b56b5 Handle use case of invalid fit's mucking things up 2015-07-10 15:46:15 -04:00
blitzmann
b8f73a7c94 bump dev 2015-07-10 12:58:03 -04:00
blitzmann
b4604f8207 Bump Stable... maybe it will be this time 2015-07-10 12:17:14 -04:00
blitzmann
68dddf2810 Fix for init projected fit. Took a long time to figure out what was happening. 2015-07-10 11:58:15 -04:00
blitzmann
4c17f38b1a Fix for #324 - do not reset itemID automatically upon removal due to way modules are swapped. 2015-07-09 18:36:11 -04:00
blitzmann
221a3fde14 Being extra cautious 2015-07-09 18:18:36 -04:00
blitzmann
c17e03d8d0 Fixes critical design issue when it comes to projected fits. Disabled some of the more advanced functionality (projection amount and active) to cope to development. Crash still happens occasionally when adding projected fit for unknown reasons - not 100% reproducable yet 2015-07-09 17:53:41 -04:00
blitzmann
af9f64db5f Move the chain into the runtime loop, otherwise projections won't work for some odd reason. 2015-07-09 14:48:15 -04:00
blitzmann
1f82465a65 Return None for price column on ship modes, fixes #322 2015-07-09 12:13:13 -04:00
blitzmann
d885bd4636 Bump dev and Fix #321 - oversight in tactical mode selection 2015-07-09 11:48:11 -04:00
blitzmann
c17bce55bb Lots of stuff
- Added logging and replaced Timer class with more useful one
- Move projected fit loop out of runtimes
- Eliminate recursions from Fit.clear()
- Clean up overall fit calc logic
2015-07-09 10:43:39 -04:00
blitzmann
4137a7cda9 Bump stable 2015-07-08 11:20:18 -04:00
blitzmann
c92911b79a Work around for lack of flag_modified() support 2015-07-08 10:54:26 -04:00
DarkPhoenix
adc9fb6d00 Don't call function multiple times after first unconditional call 2015-07-08 17:45:25 +03:00
Ryan Holmes
5baf70694a Merge pull request #318 from poettler-ric/fix-logging
Fix logging
2015-07-08 10:14:46 -04:00
Richard Poettler
f08dc97576 logging crashes if the directory doesn't exist 2015-07-08 10:47:25 +02:00
Richard Poettler
35094ae1ce moved dublicate code into one method 2015-07-08 10:45:37 +02:00
DarkPhoenix
5ac31920ee Fix #317
Crop images to square form before making thumbnail, not square images cause issues on Windows
2015-07-08 11:39:01 +03:00
DarkPhoenix
e63c3541c4 Bump for next development release 2015-07-08 00:09:34 +03:00
DarkPhoenix
4976516d4d Bump for a stable version 2015-07-07 23:36:03 +03:00
Ryan Holmes
e042a21d32 Merge pull request #315 from lunedis/resistmultiplier
Showing Resist Multiplier in Tooltip
2015-07-07 14:42:50 -04:00
Kalu
8a22907940 newline in resist multiplier tooltip and explanation 2015-07-07 20:38:27 +02:00
Kalu
a97847e644 show rr factor in tooltip 2015-07-07 20:38:16 +02:00
blitzmann
23309a5da6 Remove unneeded code that created a bitmap for checkboxes 2015-07-07 13:49:40 -04:00
blitzmann
06e4a7e80f Support changing amount of projected fits 2015-07-07 13:49:39 -04:00
blitzmann
b95a10d284 Add active column. Looping the fit to apply it x amount of times doesn't seem to work. Probably because it's been flagged calculated and returns early 2015-07-07 13:49:38 -04:00
blitzmann
2bca3ddcc8 GUI support (also made regular checkboxes pretty for drones/implant/etc) 2015-07-07 13:49:37 -04:00
blitzmann
9ef182aa99 First working prototype of toggleable projected fits. Creates a new association object that stores projection-specific information. GUI hasn't been touched (need to show state), and there are a lot of variables that I need to rename. 2015-07-07 13:49:35 -04:00
blitzmann
5e56107582 Revert changes to minimum / default sizes of splitter windows 2015-07-07 13:28:02 -04:00
DarkPhoenix
972c08e7e4 Remove AT13 prizes from ship list for now 2015-07-07 16:10:29 +03:00
DarkPhoenix
091832af21 Fix bug in script and add missing icons 2015-07-07 16:09:13 +03:00
DarkPhoenix
16d1891e16 Add script which updates renders and update them using aegis release export 2015-07-07 16:05:44 +03:00
DarkPhoenix
40ee68e2cf Update module/attribute icons 2015-07-07 15:11:55 +03:00
DarkPhoenix
bfe3b4a26d Add new entosis offline mass penalty 2015-07-07 15:10:37 +03:00
DarkPhoenix
9aa1332b15 Update database to 912410 2015-07-07 15:07:39 +03:00
DarkPhoenix
0521d242eb Make sure bitmap loader searches for proper file (w/o EVE embedfs-specific path and extension) 2015-07-06 11:31:02 +03:00
DarkPhoenix
2b3f3773e5 Merge branch 'master' into singularity 2015-07-06 01:39:50 +03:00
DarkPhoenix
4041407878 Also make sure to process icons for categories and attributes 2015-07-06 01:38:51 +03:00
blitzmann
1b5e0467fc Save browser sizes 2015-07-05 13:59:18 -04:00
DarkPhoenix
a7c346f78e Add script which is supposed to update icons 2015-07-05 20:59:15 +03:00
blitzmann
3cc51aaf89 Change logging location to ~/.pyfa and set default level to WARN 2015-07-05 13:15:26 -04:00
Ryan Holmes
a339ae1c55 Merge pull request #312 from blitzmann/dbCorruption
Fixes for database rot
2015-07-05 13:08:11 -04:00
blitzmann
3773d1c28e Improvements to fit initializations and logging 2015-07-05 12:57:04 -04:00
blitzmann
41b8db346f Fix broken drone drag 2015-07-05 01:16:53 -04:00
blitzmann
7959593c6c Improve object initialization and add support for logging the errors. 2015-07-05 00:31:52 -04:00
blitzmann
aaa60cbc14 Fix instance where some items were being re-added due to lack of return. Also, implement some basic logging. 2015-07-05 00:28:55 -04:00
DarkPhoenix
8ae5a96047 Merge branch 'master' into singularity 2015-07-04 12:45:15 +03:00
blitzmann
8c90b3132b Use lunedis's method and apply it to additions pane, cleans up code 2015-07-04 01:38:11 -04:00
blitzmann
1326e21f6b Fix IndexError when selecting module index that doesn't exist. Also, tweaked setting the selItem variable. 2015-07-04 01:27:57 -04:00
blitzmann
bfdc2161e0 Add shortcut cues as toggle in prefs 2015-07-04 00:52:44 -04:00
blitzmann
9ab79af70c Show shortcut values in market list 2015-07-04 00:44:19 -04:00
Ryan Holmes
f0de2000bf Merge pull request #313 from lunedis/quickfit
Quickly fitting modules using ALT+1-9
2015-07-04 00:34:43 -04:00
blitzmann
5991d19b3e Allow Subsystems as modules. 2015-07-04 00:32:28 -04:00
Kalu
51fed996f1 refactor quickfit shortcuts with list 2015-07-03 23:43:04 +02:00
Kalu
f6bbc6c410 Implemented using Alt+1-5 for quickly fitting modules, see issue #183 2015-07-03 23:42:54 +02:00
blitzmann
3de6b63325 Fix oversight when creating a new database 2015-07-03 14:18:11 -04:00
blitzmann
dd48815f30 Offline rigs, closes #100 2015-07-03 14:05:57 -04:00
DarkPhoenix
5608676dc8 Merge branch 'master' into singularity 2015-07-03 12:37:08 +03:00
DarkPhoenix
86ab1f7444 Ignore python pyc files and rely on index within file rather than just on amount of files 2015-07-03 12:36:17 +03:00
DarkPhoenix
8f51642f70 Merge branch 'master' into singularity 2015-07-03 12:13:53 +03:00
blitzmann
de71123a48 Merge branch 'pricing' 2015-07-03 02:38:23 -04:00
blitzmann
874cf4ef0a Use old price information if update fails. Add "(!)" to show that price is out of date 2015-07-03 02:37:52 -04:00
blitzmann
87e5929cb1 DB migration is triggered by number of upgrade files found, rather than number in config.py. This allows us to remove the db version variable in config.py and not worry about it. 2015-07-02 19:35:53 -04:00
blitzmann
84b1e0ac41 Migrate boosters table to new schema that drops the UNIQUE constraint (causes issues and is unneeded) 2015-07-02 19:34:02 -04:00
blitzmann
539360d5f6 Remove old debug print 2015-07-02 15:04:07 -04:00
blitzmann
ca08f8d8da Handle fits with invalid ships by removing and deleting them when loaded. 2015-07-02 15:03:56 -04:00
blitzmann
e1ce672569 Move flag_modified to HandledList.remove() so that it takes care of all our use cases. Give fits an itemID like everything else so that projected fits can be removed correctly by this logic. No reason for them to be special snowflakes. 2015-07-02 11:22:26 -04:00
blitzmann
717080b58c Handle invalid implants and boosters. Uses a different method to ensure implant and booster slot is not duplicated. Still need to modify existing databases to remove Booster table constraint. Reverts a previous commit: "Gracefully handle invalid boosters in database (both itemIDs that don't exist as well as non-booster items). Implants need a little more work" (aaa5a6ae18) 2015-07-02 00:48:32 -04:00
blitzmann
51696c509f Merged Cargo and Drone collection class (essentially the same). Utilized SQLAlchemy's flag_modified() to force SA to update DB (in this case, remove the entry) 2015-07-01 20:54:40 -04:00
blitzmann
4a5ae9f6f1 Handle invalid cargo. Noticed that cargo nor drones are removed from the database with these methods. Not sure why - projected drones and modules are correctly removed in similar ways 2015-07-01 15:21:27 -04:00
blitzmann
fa9f324f78 Handle invalid drones 2015-07-01 14:55:05 -04:00
blitzmann
bcc77f11cd Handle invalid projected drones 2015-07-01 14:50:08 -04:00
blitzmann
f737f292e3 Refine appending projected modules. Ensure that module can actually be projected, and also ensure that we only have 1 system effect running at a time. Invalid modules are removed at earliest opportunity as we are later accessing attributes that may not be there for corrupted data. 2015-07-01 13:34:19 -04:00
blitzmann
1c18a5207c System Effects are wrapped in Module class, even though they are not modules. Account for this. 2015-07-01 13:32:31 -04:00
blitzmann
fa2b1e3821 Handle invalid modules. This streamlines the module init code from both program and database sources. When loading from the database, we ensure that the module item is actually an item. If not, we set a flag to delete it (which is picked up by the collection class)(can't use exceptions as there's no place to catch them) 2015-07-01 02:20:56 -04:00
DarkPhoenix
6184753822 Merge branch 'master' into singularity 2015-06-30 21:16:15 +03:00
DarkPhoenix
91a9c860ea Merge branch 'singularity' of github.com:DarkFenX/Pyfa into singularity 2015-06-30 21:15:10 +03:00
DarkPhoenix
0730ac369f Update data to 910808 2015-06-30 21:14:22 +03:00
blitzmann
aaa5a6ae18 Gracefully handle invalid boosters in database (both itemIDs that don't exist as well as non-booster items). Implants need a little more work 2015-06-30 13:51:49 -04:00
blitzmann
646f3afd27 Fixed oversights 2015-06-27 19:32:21 -04:00
blitzmann
98815f2b85 Fix #307 by moving menu code to spawn event 2015-06-27 18:18:43 -04:00
Ryan Holmes
95eb5a6117 Update README.md
Added note for Linux users and wxPython 2.8
2015-06-25 21:45:06 -04:00
blitzmann
dc035469ed Fix background color for certain panels under Linux 2015-06-25 16:09:56 -04:00
DarkPhoenix
ec4a00cdfc Merge branch 'master' into singularity 2015-06-25 12:12:33 +03:00
blitzmann
21937c02ff Made it look nicer 2015-06-24 19:53:55 -04:00
DarkPhoenix
edfd446e46 Update to 908326 2015-06-25 00:33:37 +03:00
blitzmann
7ec78b941e Add help text for why api is disabled, per #269. I may or may not make it look nicer later 2015-06-24 15:10:37 -04:00
blitzmann
95bf1039c0 Fix #291 - Triage not properly implemented on projected fit 2015-06-24 14:36:31 -04:00
blitzmann
e6def6f5f9 Fix #299 - fit not recalculated after module states change upon module append 2015-06-24 00:40:13 -04:00
DarkPhoenix
de0b03630a Bump version 2015-06-23 00:17:41 +03:00
DarkPhoenix
90a2a79d5b Add hecate effects, update database to 906843 2015-06-23 00:00:09 +03:00
DarkPhoenix
21efd6d06a CCP added faction 200mm plates back to the game 2015-06-22 22:22:35 +03:00
DarkPhoenix
ea288a6133 Update conversion scripts to use new scheme 2015-06-21 16:50:07 +03:00
blitzmann
23baaa7dba Merge branch 'master' of https://github.com/DarkFenX/Pyfa 2015-06-16 13:14:28 -04:00
blitzmann
8008c986d3 Fix #302 - Projected fit applying Tactical Destroyer mode effects on projectee 2015-06-16 13:14:16 -04:00
DarkPhoenix
b9efc919ea Add missing RHML rof effect 2015-06-16 16:53:21 +03:00
blitzmann
6cc6fd9468 Instead of icon, use unicode refresh. Minor issues with image and GUI flickering 2015-06-06 22:42:42 -05:00
blitzmann
53c9169043 Simplified price pane. Will show pricing update label and will only clear it when prices are done. Removed all timer code as it makes it overly complicated and I suspect half of it didn't work as intended anyway 2015-06-05 15:39:10 -05:00
blitzmann
9e96aac04d Fix situation in which module prices are fetched individually (which the price column). Instead, have them wait in a queue that is processed when the entire fit is called and calculated (with the price pane). Also adds a little refresh icon to know that prices are updating and it's not just blank (might change) 2015-06-04 14:10:27 -05:00
DarkPhoenix
3395f8ebe6 Bump for next development release 2015-06-02 23:43:16 +03:00
DarkPhoenix
1d45102100 Bump for a stable release 2015-06-02 23:32:16 +03:00
DarkPhoenix
1694d74afa Add Carnyx tiericide module upgrade/import paths 2015-06-02 23:27:08 +03:00
DarkPhoenix
9c9f1dcefa Get faction 200mm plates back 2015-06-02 19:37:32 +03:00
DarkPhoenix
a4ca2e90f9 Add effect which affects entosis duration on cap ships 2015-06-02 16:48:58 +03:00
DarkPhoenix
dbfcfd9acf Change expansion data in config and update database to 893722 2015-06-02 16:38:38 +03:00
DarkPhoenix
8c30ee3fd3 Merge branch 'master' into singularity 2015-05-28 15:21:28 +03:00
Anton Vorobyov
ca17d17232 Merge pull request #292 from wolfwood/master
fix issue copying fits with sqlalchemy 1.0
2015-05-28 15:17:02 +03:00
wolfwood
b78c0a5845 fix issue copying fits with sqlalchemy 1.0 2015-05-27 10:21:16 -07:00
DarkPhoenix
ef6e25bfce Merge branch 'master' into singularity 2015-05-27 14:55:12 +03:00
DarkPhoenix
e81f7eb765 Change validator requirement for owner ID
This fixes bug related to sqlalch 1.0
2015-05-27 14:54:37 +03:00
955 changed files with 2094 additions and 1201 deletions

View File

@@ -6,6 +6,13 @@ It provides many advanced features such as graphs and full calculations of any p
Please see the [FAQ](https://github.com/DarkFenX/Pyfa/wiki/FAQ) for answers to common questions / concerns Please see the [FAQ](https://github.com/DarkFenX/Pyfa/wiki/FAQ) for answers to common questions / concerns
#### A note for Linux users
pyfa currently only supports wxPython 2.8. However, there are some distros that have started to support 3.0 and subsequently dropped support for 2.8 altogether (such as Debian Jessie). If this is the case and wxPython 3.0 is the only version installed, the official pyfa releases will not run. You must either find a package for 2.8 or compile it yourself.
For Debian Jessie, wxPython 2.8 is available in Sid (the unstable repo). you can use apt-pinning to install select packages from unstable and still keep your stable system. See http://jaqque.sbih.org/kplug/apt-pinning.html for me details.
3.0 support is being worked on and can be found on the wx3 branch. It may be stable enough for you, but there are a few bugs related to it. Please see the wx3 label on the GitHub issues area for me information on known issues (biggest one currently is GTK warning spam): https://github.com/DarkFenX/Pyfa/labels/wx3
#### Links #### Links
* [Development repository: http://github.com/DarkFenX/Pyfa](http://github.com/DarkFenX/Pyfa) * [Development repository: http://github.com/DarkFenX/Pyfa](http://github.com/DarkFenX/Pyfa)
* [XMPP conference: * [XMPP conference:

View File

@@ -1,6 +1,11 @@
import os import os
import sys import sys
# TODO: move all logging back to pyfa.py main loop
# We moved it here just to avoid rebuilding windows skeleton for now (any change to pyfa.py needs it)
import logging
import logging.handlers
# Load variable overrides specific to distribution type # Load variable overrides specific to distribution type
try: try:
import configforced import configforced
@@ -12,16 +17,14 @@ debug = False
# Defines if our saveddata will be in pyfa root or not # Defines if our saveddata will be in pyfa root or not
saveInRoot = False saveInRoot = False
# Version data logLevel = logging.DEBUG
version = "1.11.1"
tag = "git"
expansionName = "Singularity"
expansionVersion = "883859"
evemonMinVersion = "4081"
# Database version (int ONLY) # Version data
# Increment every time we need to flag for user database upgrade/modification version = "1.13.3"
dbversion = 7 tag = "git"
expansionName = "Aegis"
expansionVersion = "1.0"
evemonMinVersion = "4081"
pyfaPath = None pyfaPath = None
savePath = None savePath = None
@@ -29,10 +32,24 @@ staticPath = None
saveDB = None saveDB = None
gameDB = None gameDB = None
# TODO: move back to pyfa.py main loop
# We moved it here just to avoid rebuilding windows skeleton for now (any change to pyfa.py needs it) class StreamToLogger(object):
import logging """
logging.basicConfig() Fake file-like stream object that redirects writes to a logger instance.
From: http://www.electricmonk.nl/log/2011/08/14/redirect-stdout-and-stderr-to-a-logger-in-python/
"""
def __init__(self, logger, log_level=logging.INFO):
self.logger = logger
self.log_level = log_level
self.linebuf = ''
def write(self, buf):
for line in buf.rstrip().splitlines():
self.logger.log(self.log_level, line.rstrip())
def __createDirs(path):
if not os.path.exists(path):
os.makedirs(path)
def defPaths(): def defPaths():
global pyfaPath global pyfaPath
@@ -59,19 +76,25 @@ def defPaths():
savePath = unicode(os.path.expanduser(os.path.join("~", ".pyfa")), savePath = unicode(os.path.expanduser(os.path.join("~", ".pyfa")),
sys.getfilesystemencoding()) sys.getfilesystemencoding())
# Redirect stderr to file if we're requested to do so __createDirs(savePath)
stderrToFile = getattr(configforced, "stderrToFile", None)
if stderrToFile is True:
if not os.path.exists(savePath):
os.mkdir(savePath)
sys.stderr = open(os.path.join(savePath, "error_log.txt"), "w")
# Same for stdout format = '%(asctime)s %(name)-24s %(levelname)-8s %(message)s'
stdoutToFile = getattr(configforced, "stdoutToFile", None) logging.basicConfig(format=format, level=logLevel)
if stdoutToFile is True: handler = logging.handlers.RotatingFileHandler(os.path.join(savePath, "log.txt"), maxBytes=1000000, backupCount=3)
if not os.path.exists(savePath): formatter = logging.Formatter(format)
os.mkdir(savePath) handler.setFormatter(formatter)
sys.stdout = open(os.path.join(savePath, "output_log.txt"), "w") logging.getLogger('').addHandler(handler)
logging.info("Starting pyfa")
if hasattr(sys, 'frozen'):
stdout_logger = logging.getLogger('STDOUT')
sl = StreamToLogger(stdout_logger, logging.INFO)
sys.stdout = sl
stderr_logger = logging.getLogger('STDERR')
sl = StreamToLogger(stderr_logger, logging.ERROR)
sys.stderr = sl
# Static EVE Data from the staticdata repository, should be in the staticdata # Static EVE Data from the staticdata repository, should be in the staticdata
# directory in our pyfa directory # directory in our pyfa directory

View File

@@ -1,32 +1,46 @@
import config import config
import shutil import shutil
import time import time
import re
import os
def getAppVersion():
# calculate app version based on upgrade files we have
appVersion = 0
for fname in os.listdir(os.path.join(os.path.dirname(__file__), "migrations")):
m = re.match("^upgrade(?P<index>\d+)\.py$", fname)
if not m:
continue
index = int(m.group("index"))
appVersion = max(appVersion, index)
return appVersion
def getVersion(db): def getVersion(db):
cursor = db.execute('PRAGMA user_version') cursor = db.execute('PRAGMA user_version')
return cursor.fetchone()[0] return cursor.fetchone()[0]
def update(saveddata_engine): def update(saveddata_engine):
currversion = getVersion(saveddata_engine) dbVersion = getVersion(saveddata_engine)
appVersion = getAppVersion()
if currversion == config.dbversion: if dbVersion == appVersion:
return return
if currversion < config.dbversion: if dbVersion < appVersion:
# Automatically backup database # Automatically backup database
toFile = "%s/saveddata_migration_%d-%d_%s.db"%( toFile = "%s/saveddata_migration_%d-%d_%s.db"%(
config.savePath, config.savePath,
currversion, dbVersion,
config.dbversion, appVersion,
time.strftime("%Y%m%d_%H%M%S")) time.strftime("%Y%m%d_%H%M%S"))
shutil.copyfile(config.saveDB, toFile) shutil.copyfile(config.saveDB, toFile)
for version in xrange(currversion, config.dbversion): for version in xrange(dbVersion, appVersion):
module = __import__('eos.db.migrations.upgrade%d'%(version+1), fromlist=True) module = __import__("eos.db.migrations.upgrade{}".format(version + 1), fromlist=True)
upgrade = getattr(module, "upgrade", False) upgrade = getattr(module, "upgrade", False)
if upgrade: if upgrade:
upgrade(saveddata_engine) upgrade(saveddata_engine)
# when all is said and done, set version to current # when all is said and done, set version to current
saveddata_engine.execute('PRAGMA user_version = %d'%config.dbversion) saveddata_engine.execute("PRAGMA user_version = {}".format(appVersion))

View File

@@ -0,0 +1,16 @@
"""
Migration 10
- Adds active attribute to projected fits
"""
import sqlalchemy
def upgrade(saveddata_engine):
# Update projectedFits schema to include active attribute
try:
saveddata_engine.execute("SELECT active FROM projectedFits LIMIT 1")
except sqlalchemy.exc.DatabaseError:
saveddata_engine.execute("ALTER TABLE projectedFits ADD COLUMN active BOOLEAN")
saveddata_engine.execute("UPDATE projectedFits SET active = 1")
saveddata_engine.execute("UPDATE projectedFits SET amount = 1")

View File

@@ -0,0 +1,85 @@
"""
Migration 8
- Converts modules based on Carnyx Module Tiericide
Some modules have been unpublished (and unpublished module attributes are removed
from database), which causes pyfa to crash. We therefore replace these
modules with their new replacements
"""
CONVERSIONS = {
8529: ( # Large F-S9 Regolith Compact Shield Extender
8409, # Large Subordinate Screen Stabilizer I
),
8419: ( # Large Azeotropic Restrained Shield Extender
8489, # Large Supplemental Barrier Emitter I
),
8517: ( # Medium F-S9 Regolith Compact Shield Extender
8397, # Medium Subordinate Screen Stabilizer I
),
8433: ( # Medium Azeotropic Restrained Shield Extender
8477, # Medium Supplemental Barrier Emitter I
),
20627: ( # Small 'Trapper' Shield Extender
8437, # Micro Azeotropic Ward Salubrity I
8505, # Micro F-S9 Regolith Shield Induction
3849, # Micro Shield Extender I
3851, # Micro Shield Extender II
8387, # Micro Subordinate Screen Stabilizer I
8465, # Micro Supplemental Barrier Emitter I
),
8521: ( # Small F-S9 Regolith Compact Shield Extender
8401, # Small Subordinate Screen Stabilizer I
),
8427: ( # Small Azeotropic Restrained Shield Extender
8481, # Small Supplemental Barrier Emitter I
),
11343: ( # 100mm Crystalline Carbonide Restrained Plates
11345, # 100mm Reinforced Nanofiber Plates I
),
11341: ( # 100mm Rolled Tungsten Compact Plates
11339, # 100mm Reinforced Titanium Plates I
),
11327: ( # 1600mm Crystalline Carbonide Restrained Plates
11329, # 1600mm Reinforced Nanofiber Plates I
),
11325: ( # 1600mm Rolled Tungsten Compact Plates
11323, # 1600mm Reinforced Titanium Plates I
),
11351: ( # 200mm Crystalline Carbonide Restrained Plates
11353, # 200mm Reinforced Nanofiber Plates I
),
11349: ( # 200mm Rolled Tungsten Compact Plates
11347, # 200mm Reinforced Titanium Plates I
),
11311: ( # 400mm Crystalline Carbonide Restrained Plates
11313, # 400mm Reinforced Nanofiber Plates I
),
11309: ( # 400mm Rolled Tungsten Compact Plates
11307, # 400mm Reinforced Titanium Plates I
),
23791: ( # 'Citadella' 100mm Steel Plates
11335, # 50mm Reinforced Crystalline Carbonide Plates I
11337, # 50mm Reinforced Nanofiber Plates I
11333, # 50mm Reinforced Rolled Tungsten Plates I
11291, # 50mm Reinforced Steel Plates I
20343, # 50mm Reinforced Steel Plates II
11331, # 50mm Reinforced Titanium Plates I
),
11319: ( # 800mm Crystalline Carbonide Restrained Plates
11321, # 800mm Reinforced Nanofiber Plates I
),
11317: ( # 800mm Rolled Tungsten Compact Plates
11315, # 800mm Reinforced Titanium Plates I
),
}
def upgrade(saveddata_engine):
# Convert modules
for replacement_item, list in CONVERSIONS.iteritems():
for retired_item in list:
saveddata_engine.execute('UPDATE "modules" SET "itemID" = ? WHERE "itemID" = ?', (replacement_item, retired_item))
saveddata_engine.execute('UPDATE "cargo" SET "itemID" = ? WHERE "itemID" = ?', (replacement_item, retired_item))

View File

@@ -0,0 +1,23 @@
"""
Migration 9
Effectively drops UNIQUE constraint from boosters table. SQLite does not support
this, so we have to copy the table to the updated schema and then rename it
"""
tmpTable = """
CREATE TABLE boostersTemp (
'ID' INTEGER NOT NULL,
'itemID' INTEGER,
'fitID' INTEGER NOT NULL,
'active' BOOLEAN,
PRIMARY KEY(ID),
FOREIGN KEY('fitID') REFERENCES fits ('ID')
)
"""
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")
saveddata_engine.execute("DROP TABLE boosters")
saveddata_engine.execute("ALTER TABLE boostersTemp RENAME TO boosters")

View File

@@ -29,7 +29,7 @@ boosters_table = Table("boosters", saveddata_meta,
Column("itemID", Integer), Column("itemID", Integer),
Column("fitID", Integer, ForeignKey("fits.ID"), nullable = False), Column("fitID", Integer, ForeignKey("fits.ID"), nullable = False),
Column("active", Boolean), Column("active", Boolean),
UniqueConstraint("itemID", "fitID")) )
activeSideEffects_table = Table("boostersActiveSideEffects", saveddata_meta, activeSideEffects_table = Table("boostersActiveSideEffects", saveddata_meta,
Column("boosterID", ForeignKey("boosters.ID"), primary_key = True), Column("boosterID", ForeignKey("boosters.ID"), primary_key = True),

View File

@@ -17,9 +17,11 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>. # along with eos. If not, see <http://www.gnu.org/licenses/>.
#=============================================================================== #===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, String, Boolean from sqlalchemy import *
from sqlalchemy.orm import relation, mapper from sqlalchemy.orm import *
from sqlalchemy.sql import and_ from sqlalchemy.sql import and_
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm.collections import attribute_mapped_collection
from eos.db import saveddata_meta from eos.db import saveddata_meta
from eos.db.saveddata.module import modules_table from eos.db.saveddata.module import modules_table
@@ -27,9 +29,7 @@ from eos.db.saveddata.drone import drones_table
from eos.db.saveddata.cargo import cargo_table from eos.db.saveddata.cargo import cargo_table
from eos.db.saveddata.implant import fitImplants_table from eos.db.saveddata.implant import fitImplants_table
from eos.types import Fit, Module, User, Booster, Drone, Cargo, Implant, Character, DamagePattern, TargetResists from eos.types import Fit, Module, User, Booster, Drone, Cargo, Implant, Character, DamagePattern, TargetResists
from eos.effectHandlerHelpers import HandledModuleList, HandledDroneList, \ from eos.effectHandlerHelpers import *
HandledImplantBoosterList, HandledProjectedModList, HandledProjectedDroneList, \
HandledProjectedFitList, HandledCargoList
fits_table = Table("fits", saveddata_meta, fits_table = Table("fits", saveddata_meta,
Column("ID", Integer, primary_key = True), Column("ID", Integer, primary_key = True),
@@ -47,31 +47,100 @@ fits_table = Table("fits", saveddata_meta,
projectedFits_table = Table("projectedFits", saveddata_meta, projectedFits_table = Table("projectedFits", saveddata_meta,
Column("sourceID", ForeignKey("fits.ID"), primary_key = True), Column("sourceID", ForeignKey("fits.ID"), primary_key = True),
Column("victimID", ForeignKey("fits.ID"), primary_key = True), Column("victimID", ForeignKey("fits.ID"), primary_key = True),
Column("amount", Integer)) Column("amount", Integer, nullable = False, default = 1),
Column("active", Boolean, nullable = False, default = 1),
)
class ProjectedFit(object):
def __init__(self, sourceID, source_fit, amount=1, active=True):
self.sourceID = sourceID
self.source_fit = source_fit
self.amount = amount
self.active = active
@reconstructor
def init(self):
if self.source_fit.isInvalid:
# Very rare for this to happen, but be prepared for it
eos.db.saveddata_session.delete(self.source_fit)
eos.db.saveddata_session.flush()
eos.db.saveddata_session.refresh(self.victim_fit)
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)
)
mapper(Fit, fits_table, mapper(Fit, fits_table,
properties = {"_Fit__modules" : relation(Module, collection_class = HandledModuleList, properties = {
primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False), "_Fit__modules": relation(
order_by = modules_table.c.position, cascade='all, delete, delete-orphan'), Module,
"_Fit__projectedModules" : relation(Module, collection_class = HandledProjectedModList, cascade='all, delete, delete-orphan', single_parent=True, collection_class=HandledModuleList,
primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)), primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False),
"owner" : relation(User, backref = "fits"), order_by=modules_table.c.position,
"_Fit__boosters" : relation(Booster, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', single_parent=True), cascade='all, delete, delete-orphan'),
"_Fit__drones" : relation(Drone, collection_class = HandledDroneList, cascade='all, delete, delete-orphan', single_parent=True, "_Fit__projectedModules": relation(
primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)), Module,
"_Fit__cargo" : relation(Cargo, collection_class = HandledCargoList, cascade='all, delete, delete-orphan', single_parent=True, collection_class=HandledProjectedModList,
primaryjoin = and_(cargo_table.c.fitID == fits_table.c.ID)), cascade='all, delete, delete-orphan',
"_Fit__projectedDrones" : relation(Drone, collection_class = HandledProjectedDroneList, cascade='all, delete, delete-orphan', single_parent=True, single_parent=True,
primaryjoin = and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == True)), primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)),
"_Fit__implants" : relation(Implant, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', single_parent=True, "owner": relation(
primaryjoin = fitImplants_table.c.fitID == fits_table.c.ID, User,
secondaryjoin = fitImplants_table.c.implantID == Implant.ID, backref="fits"),
secondary = fitImplants_table), "itemID": fits_table.c.shipID,
"_Fit__character" : relation(Character, backref = "fits"), "shipID": fits_table.c.shipID,
"_Fit__damagePattern" : relation(DamagePattern), "_Fit__boosters": relation(
"_Fit__targetResists" : relation(TargetResists), Booster,
"_Fit__projectedFits" : relation(Fit, collection_class=HandledImplantBoosterList,
primaryjoin = projectedFits_table.c.victimID == fits_table.c.ID, cascade='all, delete, delete-orphan',
secondaryjoin = fits_table.c.ID == projectedFits_table.c.sourceID, single_parent=True),
secondary = projectedFits_table, "_Fit__drones": relation(
collection_class = HandledProjectedFitList) 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)),
"_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)),
"_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)),
"_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),
"_Fit__character": relation(
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'),
"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'),
}
)
mapper(ProjectedFit, projectedFits_table)

View File

@@ -185,6 +185,12 @@ def getFit(lookfor, eager=None):
fit = saveddata_session.query(Fit).options(*eager).filter(Fit.ID == fitID).first() fit = saveddata_session.query(Fit).options(*eager).filter(Fit.ID == fitID).first()
else: else:
raise TypeError("Need integer as argument") raise TypeError("Need integer as argument")
if fit and fit.isInvalid:
with sd_lock:
removeInvalid([fit])
return None
return fit return fit
@cachedQuery(Fleet, 1, "fleetID") @cachedQuery(Fleet, 1, "fleetID")
@@ -244,9 +250,10 @@ def getFitsWithShip(shipID, ownerID=None, where=None, eager=None):
filter = processWhere(filter, where) filter = processWhere(filter, where)
eager = processEager(eager) eager = processEager(eager)
with sd_lock: with sd_lock:
fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all())
else: else:
raise TypeError("ShipID must be integer") raise TypeError("ShipID must be integer")
return fits return fits
def getBoosterFits(ownerID=None, where=None, eager=None): def getBoosterFits(ownerID=None, where=None, eager=None):
@@ -264,7 +271,8 @@ def getBoosterFits(ownerID=None, where=None, eager=None):
filter = processWhere(filter, where) filter = processWhere(filter, where)
eager = processEager(eager) eager = processEager(eager)
with sd_lock: with sd_lock:
fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all())
return fits return fits
def countAllFits(): def countAllFits():
@@ -295,7 +303,8 @@ def countFitsWithShip(shipID, ownerID=None, where=None, eager=None):
def getFitList(eager=None): def getFitList(eager=None):
eager = processEager(eager) eager = processEager(eager)
with sd_lock: with sd_lock:
fits = saveddata_session.query(Fit).options(*eager).all() fits = removeInvalid(saveddata_session.query(Fit).options(*eager).all())
return fits return fits
def getFleetList(eager=None): def getFleetList(eager=None):
@@ -385,7 +394,8 @@ def searchFits(nameLike, where=None, eager=None):
filter = processWhere(Fit.name.like(nameLike, escape="\\"), where) filter = processWhere(Fit.name.like(nameLike, escape="\\"), where)
eager = processEager(eager) eager = processEager(eager)
with sd_lock: with sd_lock:
fits = saveddata_session.query(Fit).options(*eager).filter(filter).all() fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all())
return fits return fits
def getSquadsIDsWithFitID(fitID): def getSquadsIDsWithFitID(fitID):
@@ -406,6 +416,16 @@ def getProjectedFits(fitID):
else: else:
raise TypeError("Need integer as argument") raise TypeError("Need integer as argument")
def removeInvalid(fits):
invalids = [f for f in fits if f.isInvalid]
if invalids:
map(fits.remove, invalids)
map(saveddata_session.delete, invalids)
saveddata_session.commit()
return fits
def add(stuff): def add(stuff):
with sd_lock: with sd_lock:
saveddata_session.add(stuff) saveddata_session.add(stuff)

View File

@@ -17,8 +17,12 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>. # along with eos. If not, see <http://www.gnu.org/licenses/>.
#=============================================================================== #===============================================================================
#from sqlalchemy.orm.attributes import flag_modified
import eos.db import eos.db
import eos.types import eos.types
import logging
logger = logging.getLogger(__name__)
class HandledList(list): class HandledList(list):
def filteredItemPreAssign(self, filter, *args, **kwargs): def filteredItemPreAssign(self, filter, *args, **kwargs):
@@ -101,6 +105,14 @@ class HandledList(list):
except AttributeError: except AttributeError:
pass pass
def remove(self, thing):
# We must flag it as modified, otherwise it not be removed from the database
# @todo: flag_modified isn't in os x skel. need to rebuild to include
#flag_modified(thing, "itemID")
if thing.isInvalid: # see GH issue #324
thing.itemID = 0
list.remove(self, thing)
class HandledModuleList(HandledList): class HandledModuleList(HandledList):
def append(self, mod): def append(self, mod):
emptyPosition = float("Inf") emptyPosition = float("Inf")
@@ -115,10 +127,14 @@ class HandledModuleList(HandledList):
del self[emptyPosition] del self[emptyPosition]
mod.position = emptyPosition mod.position = emptyPosition
HandledList.insert(self, emptyPosition, mod) HandledList.insert(self, emptyPosition, mod)
if mod.isInvalid:
self.remove(mod)
return return
mod.position = len(self) mod.position = len(self)
HandledList.append(self, mod) HandledList.append(self, mod)
if mod.isInvalid:
self.remove(mod)
def insert(self, index, mod): def insert(self, index, mod):
mod.position = index mod.position = index
@@ -149,133 +165,72 @@ class HandledModuleList(HandledList):
if mod.getModifiedItemAttr("subSystemSlot") == slot: if mod.getModifiedItemAttr("subSystemSlot") == slot:
del self[i] del self[i]
class HandledDroneList(HandledList): class HandledDroneCargoList(HandledList):
def find(self, item): def find(self, item):
for d in self: for o in self:
if d.item == item: if o.item == item:
yield d yield o
def findFirst(self, item): def findFirst(self, item):
for d in self.find(item): for o in self.find(item):
return d return o
def append(self, drone): def append(self, thing):
list.append(self, drone) HandledList.append(self, thing)
def remove(self, drone): if thing.isInvalid:
HandledList.remove(self, drone) self.remove(thing)
def appendItem(self, item, amount = 1):
if amount < 1: ValueError("Amount of drones to add should be >= 1")
d = self.findFirst(item)
if d is None:
d = eos.types.Drone(item)
self.append(d)
d.amount += amount
return d
def removeItem(self, item, amount):
if amount < 1: ValueError("Amount of drones to remove should be >= 1")
d = self.findFirst(item)
if d is None: return
d.amount -= amount
if d.amount <= 0:
self.remove(d)
return None
return d
class HandledCargoList(HandledList):
# shameless copy of HandledDroneList
# I have no idea what this does, but I needed it
# @todo: investigate this
def find(self, item):
for d in self:
if d.item == item:
yield d
def findFirst(self, item):
for d in self.find(item):
return d
def append(self, cargo):
list.append(self, cargo)
def remove(self, cargo):
HandledList.remove(self, cargo)
def appendItem(self, item, qty = 1):
if qty < 1: ValueError("Amount of cargo to add should be >= 1")
d = self.findFirst(item)
if d is None:
d = eos.types.Cargo(item)
self.append(d)
d.qty += qty
return d
def removeItem(self, item, qty):
if qty < 1: ValueError("Amount of cargo to remove should be >= 1")
d = self.findFirst(item)
if d is None: return
d.qty -= qty
if d.qty <= 0:
self.remove(d)
return None
return d
class HandledImplantBoosterList(HandledList): class HandledImplantBoosterList(HandledList):
def __init__(self): def append(self, thing):
self.__slotCache = {} if thing.isInvalid:
HandledList.append(self, thing)
self.remove(thing)
return
def append(self, implant): # if needed, remove booster that was occupying slot
if self.__slotCache.has_key(implant.slot): oldObj = next((m for m in self if m.slot == thing.slot), None)
raise ValueError("Implant/Booster slot already in use, remove the old one first or set replace = True") if oldObj:
self.__slotCache[implant.slot] = implant logging.info("Slot %d occupied with %s, replacing with %s", thing.slot, oldObj.item.name, thing.item.name)
HandledList.append(self, implant) oldObj.itemID = 0 # hack to remove from DB. See GH issue #324
self.remove(oldObj)
def remove(self, implant): HandledList.append(self, thing)
HandledList.remove(self, implant)
del self.__slotCache[implant.slot]
# While we deleted this implant, in edge case seems like not all references
# to it are removed and object still lives in session; forcibly remove it,
# or otherwise when adding the same booster twice booster's table (typeID, fitID)
# constraint will report database integrity error
# TODO: make a proper fix, probably by adjusting fit-boosters sqlalchemy relationships
eos.db.remove(implant)
def freeSlot(self, slot):
if hasattr(slot, "slot"):
slot = slot.slot
try:
implant = self.__slotCache[slot]
except KeyError:
return False
try:
self.remove(implant)
except ValueError:
return False
return True
class HandledProjectedModList(HandledList): class HandledProjectedModList(HandledList):
def append(self, proj):
if proj.isInvalid:
# we must include it before we remove it. doing it this way ensures
# rows and relationships in database are removed as well
HandledList.append(self, proj)
self.remove(proj)
return
proj.projected = True
isSystemEffect = proj.item.group.name == "Effect Beacon"
if isSystemEffect:
# remove other system effects - only 1 per fit plz
oldEffect = next((m for m in self if m.item.group.name == "Effect Beacon"), None)
if oldEffect:
logging.info("System effect occupied with %s, replacing with %s", oldEffect.item.name, proj.item.name)
self.remove(oldEffect)
HandledList.append(self, proj)
# Remove non-projectable modules
if not proj.item.isType("projected") and not isSystemEffect:
self.remove(proj)
class HandledProjectedDroneList(HandledDroneCargoList):
def append(self, proj): def append(self, proj):
proj.projected = True proj.projected = True
HandledList.append(self, proj) HandledList.append(self, proj)
class HandledProjectedDroneList(HandledDroneList): # Remove invalid or non-projectable drones
def append(self, proj): if proj.isInvalid or not proj.item.isType("projected"):
proj.projected = True self.remove(proj)
list.append(self, proj)
class HandledProjectedFitList(HandledList):
def append(self, proj):
proj.projected = True
list.append(self, proj)
class HandledItem(object): class HandledItem(object):
def preAssignItemAttr(self, *args, **kwargs): def preAssignItemAttr(self, *args, **kwargs):

View File

@@ -1,7 +1,7 @@
# ammoInfluenceCapNeed # ammoInfluenceCapNeed
# #
# Used by: # Used by:
# Items from category: Charge (458 of 829) # Items from category: Charge (458 of 831)
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
# Dirty hack to work around cap charges setting cap booster # Dirty hack to work around cap charges setting cap booster

View File

@@ -1,7 +1,7 @@
# ammoInfluenceRange # ammoInfluenceRange
# #
# Used by: # Used by:
# Items from category: Charge (559 of 829) # Items from category: Charge (559 of 831)
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
module.multiplyItemAttr("maxRange", module.getModifiedChargeAttr("weaponRangeMultiplier")) module.multiplyItemAttr("maxRange", module.getModifiedChargeAttr("weaponRangeMultiplier"))

View File

@@ -1,7 +1,7 @@
# armorHPBonusAdd # armorHPBonusAdd
# #
# Used by: # Used by:
# Modules from group: Armor Reinforcer (38 of 38) # Modules from group: Armor Reinforcer (41 of 41)
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
fit.ship.increaseItemAttr("armorHP", module.getModifiedItemAttr("armorHPBonusAdd")) fit.ship.increaseItemAttr("armorHP", module.getModifiedItemAttr("armorHPBonusAdd"))

View File

@@ -1,8 +1,7 @@
# armorReinforcerMassAdd # armorReinforcerMassAdd
# #
# Used by: # Used by:
# Modules from group: Armor Reinforcer (38 of 38) # Modules from group: Armor Reinforcer (41 of 41)
# Modules from group: Entosis Link (2 of 2)
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
fit.ship.increaseItemAttr("mass", module.getModifiedItemAttr("massAddition")) fit.ship.increaseItemAttr("mass", module.getModifiedItemAttr("massAddition"))

View File

@@ -1,7 +1,7 @@
# eliteBonusHeavyInterdictorsWarpDisruptFieldGeneratorWarpScrambleRange2 # eliteBonusHeavyInterdictorsWarpDisruptFieldGeneratorWarpScrambleRange2
# #
# Used by: # Used by:
# Ships from group: Heavy Interdiction Cruiser (4 of 4) # Ships from group: Heavy Interdiction Cruiser (4 of 5)
type = "passive" type = "passive"
def handler(fit, ship, context): def handler(fit, ship, context):
level = fit.character.getSkill("Heavy Interdiction Cruisers").level level = fit.character.getSkill("Heavy Interdiction Cruisers").level

View File

@@ -0,0 +1,12 @@
# entosisDurationMultiply
#
# Used by:
# Ships from group: Carrier (4 of 4)
# Ships from group: Dreadnought (4 of 4)
# Ships from group: Supercarrier (5 of 5)
# Ships from group: Titan (4 of 4)
# Ship: Rorqual
type = "passive"
def handler(fit, ship, context):
fit.modules.filteredItemMultiply(lambda mod: mod.item.requiresSkill("Infomorph Psychology"),
"duration", ship.getModifiedItemAttr("entosisDurationMultiplier") or 1)

View File

@@ -1,7 +1,7 @@
# Interceptor2WarpScrambleRange # Interceptor2WarpScrambleRange
# #
# Used by: # Used by:
# Ships from group: Interceptor (5 of 9) # Ships from group: Interceptor (5 of 10)
type = "passive" type = "passive"
def handler(fit, ship, context): def handler(fit, ship, context):
level = fit.character.getSkill("Interceptors").level level = fit.character.getSkill("Interceptors").level

View File

@@ -1,7 +1,7 @@
# interceptorMWDSignatureRadiusBonus # interceptorMWDSignatureRadiusBonus
# #
# Used by: # Used by:
# Ships from group: Interceptor (9 of 9) # Ships from group: Interceptor (9 of 10)
type = "passive" type = "passive"
def handler(fit, ship, context): def handler(fit, ship, context):
level = fit.character.getSkill("Interceptors").level level = fit.character.getSkill("Interceptors").level

View File

@@ -0,0 +1,9 @@
# missileAOECloudSizeBonusOnline
#
# Used by:
# Modules from group: Missile Guidance Enhancer (3 of 3)
type = "passive"
def handler(fit, container, context):
fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"),
"aoeCloudSize", container.getModifiedItemAttr("aoeCloudSizeBonus"),
stackingPenalties=True)

View File

@@ -0,0 +1,9 @@
# missileAOEVelocityBonusOnline
#
# Used by:
# Modules from group: Missile Guidance Enhancer (3 of 3)
type = "passive"
def handler(fit, container, context):
fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"),
"aoeVelocity", container.getModifiedItemAttr("aoeVelocityBonus"),
stackingPenalties=True)

View File

@@ -0,0 +1,9 @@
# missileExplosionDelayBonusOnline
#
# Used by:
# Modules from group: Missile Guidance Enhancer (3 of 3)
type = "passive"
def handler(fit, container, context):
fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"),
"explosionDelay", container.getModifiedItemAttr("explosionDelayBonus"),
stackingPenalties=True)

View File

@@ -0,0 +1,15 @@
# missileGuidanceComputerBonus4
#
# Used by:
# Modules from group: Missile Guidance Computer (3 of 3)
type = "active"
def handler(fit, container, context):
for srcAttr, tgtAttr in (
("aoeCloudSizeBonus", "aoeCloudSize"),
("aoeVelocityBonus", "aoeVelocity"),
("missileVelocityBonus", "maxVelocity"),
("explosionDelayBonus", "explosionDelay"),
):
fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"),
tgtAttr, container.getModifiedItemAttr(srcAttr),
stackingPenalties=True)

View File

@@ -7,6 +7,7 @@
type = "passive" type = "passive"
def handler(fit, container, context): def handler(fit, container, context):
level = container.level if "skill" in context else 1 level = container.level if "skill" in context else 1
penalize = False if "skill" in context or "implant" in context else True
fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"),
"aoeCloudSize", container.getModifiedItemAttr("aoeCloudSizeBonus") * level, "aoeCloudSize", container.getModifiedItemAttr("aoeCloudSizeBonus") * level,
stackingPenalties = False) stackingPenalties=penalize)

View File

@@ -7,6 +7,7 @@
type = "passive" type = "passive"
def handler(fit, container, context): def handler(fit, container, context):
level = container.level if "skill" in context else 1 level = container.level if "skill" in context else 1
penalize = False if "skill" in context or "implant" in context else True
fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"), fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"),
"aoeVelocity", container.getModifiedItemAttr("aoeVelocityBonus") * level, "aoeVelocity", container.getModifiedItemAttr("aoeVelocityBonus") * level,
stackingPenalties = "skill" not in context and "implant" not in context) stackingPenalties=penalize)

View File

@@ -5,4 +5,4 @@
type = "passive" type = "passive"
def handler(fit, container, context): def handler(fit, container, context):
fit.modules.filteredChargeMultiply(lambda mod: mod.charge.requiresSkill("Defender Missiles"), fit.modules.filteredChargeMultiply(lambda mod: mod.charge.requiresSkill("Defender Missiles"),
"maxVelocity", container.getModifiedItemAttr("missileVelocityBonus")) "maxVelocity", container.getModifiedItemAttr("missileVelocityBonus"))

View File

@@ -0,0 +1,9 @@
# missileVelocityBonusOnline
#
# Used by:
# Modules from group: Missile Guidance Enhancer (3 of 3)
type = "passive"
def handler(fit, container, context):
fit.modules.filteredChargeBoost(lambda mod: mod.charge.requiresSkill("Missile Launcher Operation"),
"maxVelocity", container.getModifiedItemAttr("missileVelocityBonus"),
stackingPenalties=True)

View File

@@ -1,7 +1,7 @@
# modeAgilityPostDiv # modeAgilityPostDiv
# #
# Used by: # Used by:
# Modules named like: Propulsion Mode (3 of 3) # Modules named like: Propulsion Mode (4 of 4)
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
fit.ship.multiplyItemAttr( fit.ship.multiplyItemAttr(

View File

@@ -0,0 +1,11 @@
# modeArmorRepDurationPostDiv
#
# Used by:
# Module: Hecate Defense Mode
type = "passive"
def handler(fit, module, context):
fit.modules.filteredItemMultiply(
lambda mod: mod.item.requiresSkill("Repair Systems"),
"duration",
1 / module.getModifiedItemAttr("modeArmorRepDurationPostDiv")
)

View File

@@ -1,8 +1,7 @@
# modeArmorResonancePostDiv # modeArmorResonancePostDiv
# #
# Used by: # Used by:
# Module: Confessor Defense Mode # Modules named like: Defense Mode (3 of 4)
# Module: Svipul Defense Mode
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
for srcResType, tgtResType in ( for srcResType, tgtResType in (

View File

@@ -0,0 +1,16 @@
# modeHullResonancePostDiv
#
# Used by:
# Module: Hecate Defense Mode
type = "passive"
def handler(fit, module, context):
for srcResType, tgtResType in (
("Em", "em"),
("Explosive", "explosive"),
("Kinetic", "kinetic"),
("Thermic", "thermal")
):
fit.ship.multiplyItemAttr(
"{0}DamageResonance".format(tgtResType),
1 / module.getModifiedItemAttr("mode{0}ResistancePostDiv".format(srcResType))
)

View File

@@ -0,0 +1,13 @@
# modeMWDBoostPostDiv
#
# Used by:
# Module: Hecate Propulsion Mode
type = "passive"
def handler(fit, module, context):
fit.modules.filteredItemMultiply(
lambda mod: mod.item.requiresSkill("High Speed Maneuvering"),
"speedFactor",
1 / module.getModifiedItemAttr("modeMWDVelocityPostDiv"),
stackingPenalties=True,
penaltyGroup="postDiv"
)

View File

@@ -0,0 +1,11 @@
# modeMWDCapPostDiv
#
# Used by:
# Module: Hecate Propulsion Mode
type = "passive"
def handler(fit, module, context):
fit.modules.filteredItemMultiply(
lambda mod: mod.item.requiresSkill("High Speed Maneuvering"),
"capacitorNeed",
1 / module.getModifiedItemAttr("modeMWDCapPostDiv")
)

View File

@@ -1,7 +1,7 @@
# modeVelocityPostDiv # modeVelocityPostDiv
# #
# Used by: # Used by:
# Modules named like: Propulsion Mode (3 of 3) # Modules named like: Propulsion Mode (3 of 4)
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
fit.ship.multiplyItemAttr( fit.ship.multiplyItemAttr(

View File

@@ -1,6 +1,7 @@
# OffensiveDefensiveReduction # OffensiveDefensiveReduction
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Incursion ship attributes effects (3 of 3) # Celestials named like: Incursion ship attributes effects (3 of 3)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -0,0 +1,14 @@
# overloadSelfMissileGuidanceBonus5
#
# Used by:
# Modules from group: Missile Guidance Computer (3 of 3)
type = "overheat"
def handler(fit, module, context):
for tgtAttr in (
"aoeCloudSizeBonus",
"explosionDelayBonus",
"missileVelocityBonus",
"maxVelocityBonus",
"aoeVelocityBonus"
):
module.boostItemAttr(tgtAttr, module.getModifiedItemAttr("overloadTrackingModuleStrengthBonus"))

View File

@@ -0,0 +1,7 @@
# passiveMassAdd
#
# Used by:
# Modules from group: Entosis Link (2 of 2)
type = "offline"
def handler(fit, module, context):
fit.ship.increaseItemAttr("mass", module.getModifiedItemAttr("massAddition"))

View File

@@ -1,7 +1,7 @@
# probeLauncherCPUPercentBonusTacticalDestroyer # probeLauncherCPUPercentBonusTacticalDestroyer
# #
# Used by: # Used by:
# Ships from group: Tactical Destroyer (3 of 3) # Ships from group: Tactical Destroyer (4 of 4)
type = "passive" type = "passive"
def handler(fit, ship, context): def handler(fit, ship, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Astrometrics"), fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Astrometrics"),

View File

@@ -0,0 +1,7 @@
# scriptMissileGuidanceComputerAOECloudSizeBonusBonus
#
# Used by:
# Charges from group: Missile Guidance Script (2 of 2)
type = "passive"
def handler(fit, module, context):
module.boostItemAttr("aoeCloudSizeBonus", module.getModifiedChargeAttr("aoeCloudSizeBonusBonus"))

View File

@@ -0,0 +1,7 @@
# scriptMissileGuidanceComputerAOEVelocityBonusBonus
#
# Used by:
# Charges from group: Missile Guidance Script (2 of 2)
type = "passive"
def handler(fit, module, context):
module.boostItemAttr("aoeVelocityBonus", module.getModifiedChargeAttr("aoeVelocityBonusBonus"))

View File

@@ -0,0 +1,7 @@
# scriptMissileGuidanceComputerExplosionDelayBonusBonus
#
# Used by:
# Charges from group: Missile Guidance Script (2 of 2)
type = "passive"
def handler(fit, module, context):
module.boostItemAttr("explosionDelayBonus", module.getModifiedChargeAttr("explosionDelayBonusBonus"))

View File

@@ -0,0 +1,7 @@
# scriptMissileGuidanceComputerMissileVelocityBonusBonus
#
# Used by:
# Charges from group: Missile Guidance Script (2 of 2)
type = "passive"
def handler(fit, module, context):
module.boostItemAttr("missileVelocityBonus", module.getModifiedChargeAttr("missileVelocityBonusBonus"))

View File

@@ -2,6 +2,7 @@
# #
# Used by: # Used by:
# Ship: Daredevil # Ship: Daredevil
# Ship: Hecate
type = "passive" type = "passive"
def handler(fit, ship, context): def handler(fit, ship, context):
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Hybrid Turret"), fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Hybrid Turret"),

View File

@@ -0,0 +1,9 @@
# shipBonusRHMLROFCB
#
# Used by:
# Ship: Scorpion Navy Issue
type = "passive"
def handler(fit, ship, context):
level = fit.character.getSkill("Caldari Battleship").level
fit.modules.filteredItemBoost(lambda mod: mod.item.group.name == "Missile Launcher Rapid Heavy",
"speed", ship.getModifiedItemAttr("shipBonusCB") * level)

View File

@@ -1,7 +1,7 @@
# shipCapPropulsionJamming # shipCapPropulsionJamming
# #
# Used by: # Used by:
# Ships from group: Interceptor (9 of 9) # Ships from group: Interceptor (9 of 10)
# Ship: Atron # Ship: Atron
# Ship: Condor # Ship: Condor
# Ship: Executioner # Ship: Executioner

View File

@@ -0,0 +1,9 @@
# shipHeatDamageGallenteTacticalDestroyer3
#
# Used by:
# Ship: Hecate
type = "passive"
def handler(fit, ship, context):
level = fit.character.getSkill("Gallente Tactical Destroyer").level
fit.modules.filteredItemBoost(lambda mod: True, "heatDamage",
ship.getModifiedItemAttr("shipBonusTacticalDestroyerGallente3") * level)

View File

@@ -1,7 +1,7 @@
# shipModeMaxTargetRangePostDiv # shipModeMaxTargetRangePostDiv
# #
# Used by: # Used by:
# Modules named like: Sharpshooter Mode (3 of 3) # Modules named like: Sharpshooter Mode (4 of 4)
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
fit.ship.multiplyItemAttr( fit.ship.multiplyItemAttr(

View File

@@ -1,7 +1,7 @@
# shipModeScanResPostDiv # shipModeScanResPostDiv
# #
# Used by: # Used by:
# Modules named like: Sharpshooter Mode (3 of 3) # Modules named like: Sharpshooter Mode (4 of 4)
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
fit.ship.multiplyItemAttr( fit.ship.multiplyItemAttr(

View File

@@ -1,7 +1,7 @@
# shipModeScanStrengthPostDiv # shipModeScanStrengthPostDiv
# #
# Used by: # Used by:
# Modules named like: Sharpshooter Mode (3 of 3) # Modules named like: Sharpshooter Mode (4 of 4)
type = "passive" type = "passive"
def handler(fit, module, context): def handler(fit, module, context):
for scanType in ("Gravimetric", "Magnetometric", "Radar", "Ladar"): for scanType in ("Gravimetric", "Magnetometric", "Radar", "Ladar"):

View File

@@ -0,0 +1,13 @@
# shipModeSHTOptimalRangePostDiv
#
# Used by:
# Module: Hecate Sharpshooter Mode
type = "passive"
def handler(fit, module, context):
fit.modules.filteredItemMultiply(
lambda mod: mod.item.requiresSkill("Small Hybrid Turret"),
"maxRange",
1 / module.getModifiedItemAttr("modeMaxRangePostDiv"),
stackingPenalties=True,
penaltyGroup="postDiv"
)

View File

@@ -0,0 +1,9 @@
# shipSHTRoFGallenteTacticalDestroyer1
#
# Used by:
# Ship: Hecate
type = "passive"
def handler(fit, ship, context):
level = fit.character.getSkill("Gallente Tactical Destroyer").level
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Hybrid Turret"),
"speed", ship.getModifiedItemAttr("shipBonusTacticalDestroyerGallente1") * level)

View File

@@ -0,0 +1,9 @@
# shipSHTTrackingGallenteTacticalDestroyer2
#
# Used by:
# Ship: Hecate
type = "passive"
def handler(fit, ship, context):
level = fit.character.getSkill("Gallente Tactical Destroyer").level
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Small Hybrid Turret"),
"trackingSpeed", ship.getModifiedItemAttr("shipBonusTacticalDestroyerGallente2") * level)

View File

@@ -1,6 +1,7 @@
# systemDamageDrones # systemDamageDrones
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Magnetar Effect Beacon Class (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -1,6 +1,7 @@
# systemDamageEmMissiles # systemDamageEmMissiles
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Magnetar Effect Beacon Class (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -1,6 +1,7 @@
# systemDamageExplosiveMissiles # systemDamageExplosiveMissiles
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Magnetar Effect Beacon Class (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -1,6 +1,7 @@
# systemDamageKineticMissiles # systemDamageKineticMissiles
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Magnetar Effect Beacon Class (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -1,6 +1,7 @@
# systemDamageMultiplierGunnery # systemDamageMultiplierGunnery
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Magnetar Effect Beacon Class (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -1,6 +1,7 @@
# systemDamageThermalMissiles # systemDamageThermalMissiles
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Magnetar Effect Beacon Class (6 of 6) # Celestials named like: Magnetar Effect Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -2,6 +2,7 @@
# #
# Used by: # Used by:
# Celestials named like: Black Hole Effect Beacon Class (6 of 6) # Celestials named like: Black Hole Effect Beacon Class (6 of 6)
# Celestials named like: Drifter Incursion (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")
def handler(fit, beacon, context): def handler(fit, beacon, context):

View File

@@ -1,6 +1,7 @@
# systemSmartBombEmDamage # systemSmartBombEmDamage
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Red Giant Beacon Class (6 of 6) # Celestials named like: Red Giant Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -1,6 +1,7 @@
# systemSmartBombExplosiveDamage # systemSmartBombExplosiveDamage
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Red Giant Beacon Class (6 of 6) # Celestials named like: Red Giant Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -1,6 +1,7 @@
# systemSmartBombKineticDamage # systemSmartBombKineticDamage
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Red Giant Beacon Class (6 of 6) # Celestials named like: Red Giant Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -1,6 +1,7 @@
# systemSmartBombThermalDamage # systemSmartBombThermalDamage
# #
# Used by: # Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Red Giant Beacon Class (6 of 6) # Celestials named like: Red Giant Beacon Class (6 of 6)
runTime = "early" runTime = "early"
type = ("projected", "offline") type = ("projected", "offline")

View File

@@ -3,6 +3,7 @@
# Used by: # Used by:
# Module: Triage Module I # Module: Triage Module I
type = "active" type = "active"
runTime = "early"
def handler(fit, module, context): def handler(fit, module, context):
# Remote armor reps # Remote armor reps
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capital Remote Armor Repair Systems"), fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capital Remote Armor Repair Systems"),

View File

@@ -3,6 +3,7 @@
# Used by: # Used by:
# Module: Triage Module II # Module: Triage Module II
type = "active" type = "active"
runTime = "early"
def handler(fit, module, context): def handler(fit, module, context):
# Remote armor reps # Remote armor reps
fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capital Remote Armor Repair Systems"), fit.modules.filteredItemBoost(lambda mod: mod.item.requiresSkill("Capital Remote Armor Repair Systems"),

View File

@@ -20,36 +20,53 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut
from eos.effectHandlerHelpers import HandledItem from eos.effectHandlerHelpers import HandledItem
from sqlalchemy.orm import reconstructor, validates from sqlalchemy.orm import reconstructor, validates
import eos.db
import logging
logger = logging.getLogger(__name__)
class Booster(HandledItem, ItemAttrShortcut): class Booster(HandledItem, ItemAttrShortcut):
def __init__(self, item): def __init__(self, item):
self.__slot = self.__calculateSlot(item)
self.itemID = item.ID
self.__item = item self.__item = item
if self.isInvalid:
raise ValueError("Passed item is not a Booster")
self.itemID = item.ID if item is not None else None
self.active = True self.active = True
self.build() self.build()
@reconstructor @reconstructor
def init(self): def init(self):
"""Initialize a booster from the database and validate"""
self.__item = None self.__item = None
if self.itemID:
self.__item = eos.db.getItem(self.itemID)
if self.__item is None:
logger.error("Item (id: %d) does not exist", self.itemID)
return
if self.isInvalid:
logger.error("Item (id: %d) is not a Booser", self.itemID)
return
self.build()
def build(self): def build(self):
""" Build object. Assumes proper and valid item already set """
self.__sideEffects = []
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes self.__itemModifiedAttributes.original = self.__item.attributes
self.__sideEffects = [] self.__slot = self.__calculateSlot(self.__item)
for effect in self.item.effects.itervalues():
for effect in self.__item.effects.itervalues():
if effect.isType("boosterSideEffect"): if effect.isType("boosterSideEffect"):
s = SideEffect(self) s = SideEffect(self)
s.effect = effect s.effect = effect
s.active = effect.ID in self.__activeSideEffectIDs s.active = effect.ID in self.__activeSideEffectIDs
self.__sideEffects.append(s) self.__sideEffects.append(s)
def __fetchItemInfo(self):
import eos.db
self.__item = eos.db.getItem(self.itemID)
self.__slot = self.__calculateSlot(self.__item)
self.build()
def iterSideEffects(self): def iterSideEffects(self):
return self.__sideEffects.__iter__() return self.__sideEffects.__iter__()
@@ -62,23 +79,18 @@ class Booster(HandledItem, ItemAttrShortcut):
@property @property
def itemModifiedAttributes(self): def itemModifiedAttributes(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__itemModifiedAttributes return self.__itemModifiedAttributes
@property @property
def slot(self): def isInvalid(self):
if self.__item is None: return self.__item is None or self.__item.group.name != "Booster"
self.__fetchItemInfo()
@property
def slot(self):
return self.__slot return self.__slot
@property @property
def item(self): def item(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__item return self.__item
def __calculateSlot(self, item): def __calculateSlot(self, item):

View File

@@ -20,40 +20,45 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.effectHandlerHelpers import HandledItem, HandledCharge
from sqlalchemy.orm import validates, reconstructor from sqlalchemy.orm import validates, reconstructor
import eos.db
import logging
# Cargo class copied from Implant class and hacked to make work. \o/ logger = logging.getLogger(__name__)
# @todo: clean me up, Scotty
class Cargo(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): class Cargo(HandledItem, ItemAttrShortcut):
def __init__(self, item): def __init__(self, item):
"""Initialize cargo from the program"""
self.__item = item self.__item = item
self.itemID = item.ID self.itemID = item.ID if item is not None else None
self.amount = 0 self.amount = 0
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.item.attributes self.__itemModifiedAttributes.original = item.attributes
@reconstructor @reconstructor
def init(self): def init(self):
"""Initialize cargo from the database and validate"""
self.__item = None self.__item = None
def __fetchItemInfo(self): if self.itemID:
import eos.db self.__item = eos.db.getItem(self.itemID)
self.__item = eos.db.getItem(self.itemID) if self.__item is None:
logger.error("Item (id: %d) does not exist", self.itemID)
return
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes self.__itemModifiedAttributes.original = self.__item.attributes
@property @property
def itemModifiedAttributes(self): def itemModifiedAttributes(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__itemModifiedAttributes return self.__itemModifiedAttributes
@property @property
def item(self): def isInvalid(self):
if self.__item is None: return self.__item is None
self.__fetchItemInfo()
@property
def item(self):
return self.__item return self.__item
def clear(self): def clear(self):

View File

@@ -192,7 +192,7 @@ class Character(object):
map = {"ID": lambda val: isinstance(val, int), map = {"ID": lambda val: isinstance(val, int),
"name" : lambda val: True, "name" : lambda val: True,
"apiKey" : lambda val: val is None or (isinstance(val, basestring) and len(val) > 0), "apiKey" : lambda val: val is None or (isinstance(val, basestring) and len(val) > 0),
"ownerID" : lambda val: isinstance(val, int)} "ownerID" : lambda val: isinstance(val, int) or val is None}
if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key) if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key)
else: return val else: return val

View File

@@ -20,81 +20,80 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.effectHandlerHelpers import HandledItem, HandledCharge
from sqlalchemy.orm import validates, reconstructor from sqlalchemy.orm import validates, reconstructor
import eos.db
import logging
logger = logging.getLogger(__name__)
class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal") DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal")
MINING_ATTRIBUTES = ("miningAmount",) MINING_ATTRIBUTES = ("miningAmount",)
def __init__(self, item): def __init__(self, item):
if item.category.name != "Drone": """Initialize a drone from the program"""
raise ValueError("Passed item is not a drone")
self.__item = item self.__item = item
self.__charge = None
self.itemID = item.ID if self.isInvalid:
raise ValueError("Passed item is not a Drone")
self.itemID = item.ID if item is not None else None
self.amount = 0 self.amount = 0
self.amountActive = 0 self.amountActive = 0
self.__dps = None
self.__volley = None
self.__miningyield = None
self.projected = False self.projected = False
self.__itemModifiedAttributes = ModifiedAttributeDict() self.build()
self.itemModifiedAttributes.original = self.item.attributes
@reconstructor @reconstructor
def init(self): def init(self):
"""Initialize a drone from the database and validate"""
self.__item = None
if self.itemID:
self.__item = eos.db.getItem(self.itemID)
if self.__item is None:
logger.error("Item (id: %d) does not exist", self.itemID)
return
if self.isInvalid:
logger.error("Item (id: %d) is not a Drone", self.itemID)
return
self.build()
def build(self):
""" Build object. Assumes proper and valid item already set """
self.__charge = None
self.__dps = None self.__dps = None
self.__volley = None self.__volley = None
self.__miningyield = None self.__miningyield = None
self.__item = None
self.__charge = None
def __fetchItemInfo(self):
import eos.db
self.__item = eos.db.getItem(self.itemID)
self.__charge = None
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.item.attributes self.__itemModifiedAttributes.original = self.__item.attributes
def __fetchChargeInfo(self):
chargeID = self.getModifiedItemAttr("entityMissileTypeID")
self.__chargeModifiedAttributes = ModifiedAttributeDict() self.__chargeModifiedAttributes = ModifiedAttributeDict()
chargeID = self.getModifiedItemAttr("entityMissileTypeID")
if chargeID is not None: if chargeID is not None:
import eos.db
charge = eos.db.getItem(int(chargeID)) charge = eos.db.getItem(int(chargeID))
self.__charge = charge self.__charge = charge
self.__chargeModifiedAttributes.original = charge.attributes
self.chargeModifiedAttributes.original = charge.attributes
else:
self.__charge = 0
@property @property
def itemModifiedAttributes(self): def itemModifiedAttributes(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__itemModifiedAttributes return self.__itemModifiedAttributes
@property @property
def chargeModifiedAttributes(self): def chargeModifiedAttributes(self):
if self.__charge is None:
self.__fetchChargeInfo()
return self.__chargeModifiedAttributes return self.__chargeModifiedAttributes
@property @property
def item(self): def isInvalid(self):
if self.__item is None: return self.__item is None or self.__item.category.name != "Drone"
self.__fetchItemInfo()
@property
def item(self):
return self.__item return self.__item
@property @property
def charge(self): def charge(self):
if self.__charge is None: return self.__charge
self.__fetchChargeInfo()
return self.__charge if self.__charge != 0 else None
@property @property
def dealsDamage(self): def dealsDamage(self):

View File

@@ -17,8 +17,7 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>. # along with eos. If not, see <http://www.gnu.org/licenses/>.
#=============================================================================== #===============================================================================
from eos.effectHandlerHelpers import HandledList, HandledModuleList, HandledDroneList, HandledImplantBoosterList, \ from eos.effectHandlerHelpers import *
HandledProjectedFitList, HandledProjectedModList, HandledProjectedDroneList, HandledCargoList
from eos.modifiedAttributeDict import ModifiedAttributeDict from eos.modifiedAttributeDict import ModifiedAttributeDict
from sqlalchemy.orm import validates, reconstructor from sqlalchemy.orm import validates, reconstructor
from itertools import chain from itertools import chain
@@ -28,7 +27,14 @@ from math import sqrt, log, asinh
from eos.types import Drone, Cargo, Ship, Character, State, Slot, Module, Implant, Booster, Skill from eos.types import Drone, Cargo, Ship, Character, State, Slot, Module, Implant, Booster, Skill
from eos.saveddata.module import State from eos.saveddata.module import State
from eos.saveddata.mode import Mode from eos.saveddata.mode import Mode
import eos.db
import time import time
import copy
from utils.timer import Timer
import logging
logger = logging.getLogger(__name__)
try: try:
from collections import OrderedDict from collections import OrderedDict
@@ -48,34 +54,57 @@ class Fit(object):
PEAK_RECHARGE = 0.25 PEAK_RECHARGE = 0.25
def __init__(self): def __init__(self, ship=None, name=""):
"""Initialize a fit from the program"""
# use @mode.setter's to set __attr and IDs. This will set mode as well
self.ship = ship
self.__modules = HandledModuleList() self.__modules = HandledModuleList()
self.__drones = HandledDroneList() self.__drones = HandledDroneCargoList()
self.__cargo = HandledCargoList() self.__cargo = HandledDroneCargoList()
self.__implants = HandledImplantBoosterList() self.__implants = HandledImplantBoosterList()
self.__boosters = HandledImplantBoosterList() self.__boosters = HandledImplantBoosterList()
self.__projectedFits = HandledProjectedFitList() #self.__projectedFits = {}
self.__projectedModules = HandledProjectedModList() self.__projectedModules = HandledProjectedModList()
self.__projectedDrones = HandledProjectedDroneList() self.__projectedDrones = HandledProjectedDroneList()
self.__character = None self.__character = None
self.__owner = None self.__owner = None
self.shipID = None
self.projected = False self.projected = False
self.name = "" self.name = name
self.fleet = None
self.boostsFits = set()
self.gangBoosts = None
self.timestamp = time.time() self.timestamp = time.time()
self.ecmProjectedStr = 1
self.modeID = None self.modeID = None
self.build() self.build()
@reconstructor @reconstructor
def init(self): def init(self):
"""Initialize a fit from the database and validate"""
self.__ship = None
self.__mode = None
if self.shipID:
item = eos.db.getItem(self.shipID)
if item is None:
logger.error("Item (id: %d) does not exist", self.shipID)
return
try:
self.__ship = Ship(item)
except ValueError:
logger.error("Item (id: %d) is not a Ship", self.shipID)
return
if self.modeID and self.__ship:
item = eos.db.getItem(self.modeID)
# Don't need to verify if it's a proper item, as validateModeItem assures this
self.__mode = self.ship.validateModeItem(item)
else:
self.__mode = self.ship.validateModeItem(None)
self.build() self.build()
def build(self): def build(self):
from eos import db
self.__extraDrains = [] self.__extraDrains = []
self.__ehp = None self.__ehp = None
self.__weaponDPS = None self.__weaponDPS = None
@@ -100,11 +129,6 @@ class Fit(object):
self.ecmProjectedStr = 1 self.ecmProjectedStr = 1
self.extraAttributes = ModifiedAttributeDict(self) self.extraAttributes = ModifiedAttributeDict(self)
self.extraAttributes.original = self.EXTRA_ATTRIBUTES self.extraAttributes.original = self.EXTRA_ATTRIBUTES
self.ship = Ship(db.getItem(self.shipID)) if self.shipID is not None else None
if self.ship is not None:
self.mode = self.ship.checkModeItem(db.getItem(self.modeID) if self.modeID else None)
else:
self.mode = None
@property @property
def targetResists(self): def targetResists(self):
@@ -128,13 +152,17 @@ class Fit(object):
self.__ehp = None self.__ehp = None
self.__effectiveTank = None self.__effectiveTank = None
@property
def isInvalid(self):
return self.__ship is None
@property @property
def mode(self): def mode(self):
return self._mode return self.__mode
@mode.setter @mode.setter
def mode(self, mode): def mode(self, mode):
self._mode = mode self.__mode = mode
self.modeID = mode.item.ID if mode is not None else None self.modeID = mode.item.ID if mode is not None else None
@property @property
@@ -154,7 +182,7 @@ class Fit(object):
self.__ship = ship self.__ship = ship
self.shipID = ship.item.ID if ship is not None else None self.shipID = ship.item.ID if ship is not None else None
# set mode of new ship # set mode of new ship
self.mode = self.ship.checkModeItem(None) if ship is not None else None self.mode = self.ship.validateModeItem(None) if ship is not None else None
@property @property
def drones(self): def drones(self):
@@ -182,7 +210,12 @@ class Fit(object):
@property @property
def projectedFits(self): def projectedFits(self):
return self.__projectedFits # only in extreme edge cases will the fit be invalid, but to be sure do
# not return them.
return [fit for fit in self.__projectedFits.values() if not fit.isInvalid]
def getProjectionInfo(self, fitID):
return self.projectedOnto.get(fitID, None)
@property @property
def projectedDrones(self): def projectedDrones(self):
@@ -295,13 +328,13 @@ class Fit(object):
@validates("ID", "ownerID", "shipID") @validates("ID", "ownerID", "shipID")
def validator(self, key, val): def validator(self, key, val):
map = {"ID": lambda val: isinstance(val, int), map = {"ID": lambda val: isinstance(val, int),
"ownerID" : lambda val: isinstance(val, int), "ownerID" : lambda val: isinstance(val, int) or val is None,
"shipID" : lambda val: isinstance(val, int) or val is None} "shipID" : lambda val: isinstance(val, int) or val is None}
if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key) if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key)
else: return val else: return val
def clear(self): def clear(self, projected=False):
self.__effectiveTank = None self.__effectiveTank = None
self.__weaponDPS = None self.__weaponDPS = None
self.__minerYield = None self.__minerYield = None
@@ -321,10 +354,30 @@ class Fit(object):
del self.__calculatedTargets[:] del self.__calculatedTargets[:]
del self.__extraDrains[:] del self.__extraDrains[:]
if self.ship is not None: self.ship.clear() if self.ship:
c = chain(self.modules, self.drones, self.boosters, self.implants, self.projectedDrones, self.projectedModules, self.projectedFits, (self.character, self.extraAttributes)) self.ship.clear()
c = chain(
self.modules,
self.drones,
self.boosters,
self.implants,
self.projectedDrones,
self.projectedModules,
(self.character, self.extraAttributes),
)
for stuff in c: for stuff in c:
if stuff is not None and stuff != self: stuff.clear() if stuff is not None and stuff != self:
stuff.clear()
# If this is the active fit that we are clearing, not a projected fit,
# then this will run and clear the projected ships and flag the next
# iteration to skip this part to prevent recursion.
if not projected:
for stuff in self.projectedFits:
if stuff is not None and stuff != self:
stuff.clear(projected=True)
#Methods to register and get the thing currently affecting the fit, #Methods to register and get the thing currently affecting the fit,
#so we can correctly map "Affected By" #so we can correctly map "Affected By"
@@ -338,7 +391,48 @@ class Fit(object):
def getModifier(self): def getModifier(self):
return self.__modifier return self.__modifier
def __calculateGangBoosts(self, runTime):
for name, info in self.gangBoosts.iteritems():
# Unpack all data required to run effect properly
effect, thing = info[1]
if effect.runTime == runTime:
context = ("gang", thing.__class__.__name__.lower())
if isinstance(thing, Module):
if effect.isType("offline") or (effect.isType("passive") and thing.state >= State.ONLINE) or \
(effect.isType("active") and thing.state >= State.ACTIVE):
# Run effect, and get proper bonuses applied
try:
self.register(thing)
effect.handler(self, thing, context)
except:
pass
else:
# Run effect, and get proper bonuses applied
try:
self.register(thing)
effect.handler(self, thing, context)
except:
pass
def calculateModifiedAttributes(self, targetFit=None, withBoosters=False, dirtyStorage=None): def calculateModifiedAttributes(self, targetFit=None, withBoosters=False, dirtyStorage=None):
timer = Timer('Fit: {}, {}'.format(self.ID, self.name), logger)
logger.debug("Starting fit calculation on: %d %s (%s)" %
(self.ID, self.name, self.ship.item.name))
shadow = False
if targetFit:
logger.debug("Applying projections to target: %d %s (%s)",
targetFit.ID, targetFit.name, targetFit.ship.item.name)
projectionInfo = self.getProjectionInfo(targetFit.ID)
logger.debug("ProjectionInfo: %s", ', '.join("%s: %s" % item for item in vars(projectionInfo).items()))
if self == targetFit:
shadow = True
self = copy.deepcopy(self)
logger.debug("Handling self projection - making shadow copy of fit. %s => %s", projectionInfo.source_fit, self)
# we rollback 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 we can immediately rollback
eos.db.saveddata_session.rollback()
refreshBoosts = False refreshBoosts = False
if withBoosters is True: if withBoosters is True:
refreshBoosts = True refreshBoosts = True
@@ -347,6 +441,7 @@ class Fit(object):
if dirtyStorage is not None: if dirtyStorage is not None:
dirtyStorage.update(self.boostsFits) dirtyStorage.update(self.boostsFits)
if self.fleet is not None and refreshBoosts is True: if self.fleet is not None and refreshBoosts is True:
logger.debug("Fleet is set, gathering gang boosts")
self.gangBoosts = self.fleet.recalculateLinear(withBoosters=withBoosters, dirtyStorage=dirtyStorage) self.gangBoosts = self.fleet.recalculateLinear(withBoosters=withBoosters, dirtyStorage=dirtyStorage)
elif self.fleet is None: elif self.fleet is None:
self.gangBoosts = None self.gangBoosts = None
@@ -355,80 +450,64 @@ class Fit(object):
dirtyStorage.remove(self.ID) dirtyStorage.remove(self.ID)
except KeyError: except KeyError:
pass pass
# If we're not explicitly asked to project fit onto something, # If we're not explicitly asked to project fit onto something,
# set self as target fit # set self as target fit
if targetFit is None: if targetFit is None:
targetFit = self targetFit = self
forceProjected = False projected = False
# Else, we're checking all target projectee fits
elif targetFit not in self.__calculatedTargets:
self.__calculatedTargets.append(targetFit)
targetFit.calculateModifiedAttributes(dirtyStorage=dirtyStorage)
forceProjected = True
# Or do nothing if target fit is calculated
else: else:
return projected = True
# If fit is calculated and we have nothing to do here, get out # If fit is calculated and we have nothing to do here, get out
if self.__calculated == True and forceProjected == False: if self.__calculated and not projected:
logger.debug("Fit has already been calculated and is not projected, returning")
return return
# Mark fit as calculated # Mark fit as calculated
self.__calculated = True self.__calculated = True
# There's a few things to keep in mind here
# 1: Early effects first, then regular ones, then late ones, regardless of anything else
# 2: Some effects aren't implemented
# 3: Some effects are implemented poorly and will just explode on us
# 4: Errors should be handled gracefully and preferably without crashing unless serious
for runTime in ("early", "normal", "late"): for runTime in ("early", "normal", "late"):
# Build a little chain of stuff c = chain(
# Avoid adding projected drones and modules when fit is projected onto self (self.character, self.ship),
# TODO: remove this workaround when proper self-projection using virtual duplicate fits is implemented self.drones,
if forceProjected is True: self.boosters,
c = chain((self.character, self.ship, self.mode), self.drones, self.boosters, self.appliedImplants, self.modules) self.appliedImplants,
else: self.modules
c = chain((self.character, self.ship, self.mode), self.drones, self.boosters, self.appliedImplants, self.modules, )
self.projectedDrones, self.projectedModules)
if not projected:
# if not a projected fit, add a couple of more things
c = chain(c, self.projectedDrones, self.projectedModules)
# We calculate gang bonuses first so that projected fits get them
if self.gangBoosts is not None: if self.gangBoosts is not None:
contextMap = {Skill: "skill", self.__calculateGangBoosts(runTime)
Ship: "ship",
Module: "module",
Implant: "implant"}
for name, info in self.gangBoosts.iteritems():
# Unpack all data required to run effect properly
effect, thing = info[1]
if effect.runTime == runTime:
context = ("gang", contextMap[type(thing)])
if isinstance(thing, Module):
if effect.isType("offline") or (effect.isType("passive") and thing.state >= State.ONLINE) or \
(effect.isType("active") and thing.state >= State.ACTIVE):
# Run effect, and get proper bonuses applied
try:
self.register(thing)
effect.handler(self, thing, context)
except:
pass
else:
# Run effect, and get proper bonuses applied
try:
self.register(thing)
effect.handler(self, thing, context)
except:
pass
for item in c: for item in c:
# Registering the item about to affect the fit allows us to track "Affected By" relations correctly # Registering the item about to affect the fit allows us to
# track "Affected By" relations correctly
if item is not None: if item is not None:
self.register(item) self.register(item)
item.calculateModifiedAttributes(self, runTime, False) item.calculateModifiedAttributes(self, runTime, False)
if forceProjected is True: if projected is True:
targetFit.register(item) for _ in xrange(projectionInfo.amount):
item.calculateModifiedAttributes(targetFit, runTime, True) targetFit.register(item)
item.calculateModifiedAttributes(targetFit, runTime, True)
for fit in self.projectedFits: timer.checkpoint('Done with runtime: %s'%runTime)
fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage)
# Only apply projected fits if fit it not projected itself.
if not projected:
for fit in self.projectedFits:
if fit.getProjectionInfo(self.ID).active:
fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage)
timer.checkpoint('Done with fit calculation')
if shadow:
logger.debug("Delete shadow fit object")
del self
def fill(self): def fill(self):
""" """
@@ -901,6 +980,9 @@ class Fit(object):
c.append(deepcopy(i, memo)) c.append(deepcopy(i, memo))
for fit in self.projectedFits: for fit in self.projectedFits:
copy.projectedFits.append(fit) copy.__projectedFits[fit.ID] = fit
# this bit is required -- see GH issue # 83
eos.db.saveddata_session.flush()
eos.db.saveddata_session.refresh(fit)
return copy return copy

View File

@@ -20,46 +20,58 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut
from eos.effectHandlerHelpers import HandledItem from eos.effectHandlerHelpers import HandledItem
from sqlalchemy.orm import validates, reconstructor from sqlalchemy.orm import validates, reconstructor
import eos.db
import logging
logger = logging.getLogger(__name__)
class Implant(HandledItem, ItemAttrShortcut): class Implant(HandledItem, ItemAttrShortcut):
def __init__(self, item): def __init__(self, item):
self.__slot = self.__calculateSlot(item)
self.__item = item self.__item = item
self.itemID = item.ID
if self.isInvalid:
raise ValueError("Passed item is not an Implant")
self.itemID = item.ID if item is not None else None
self.active = True self.active = True
self.__itemModifiedAttributes = ModifiedAttributeDict() self.build()
self.__itemModifiedAttributes.original = self.item.attributes
@reconstructor @reconstructor
def init(self): def init(self):
self.__item = None self.__item = None
def __fetchItemInfo(self): if self.itemID:
import eos.db self.__item = eos.db.getItem(self.itemID)
self.__item = eos.db.getItem(self.itemID) if self.__item is None:
logger.error("Item (id: %d) does not exist", self.itemID)
return
if self.isInvalid:
logger.error("Item (id: %d) is not an Implant", self.itemID)
return
self.build()
def build(self):
""" Build object. Assumes proper and valid item already set """
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes self.__itemModifiedAttributes.original = self.__item.attributes
self.__slot = self.__calculateSlot(self.__item) self.__slot = self.__calculateSlot(self.__item)
@property @property
def itemModifiedAttributes(self): def itemModifiedAttributes(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__itemModifiedAttributes return self.__itemModifiedAttributes
@property @property
def slot(self): def isInvalid(self):
if self.__item is None: return self.__item is None or self.__item.category.name != "Implant"
self.__fetchItemInfo()
@property
def slot(self):
return self.__slot return self.__slot
@property @property
def item(self): def item(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__item return self.__item
def __calculateSlot(self, item): def __calculateSlot(self, item):

View File

@@ -19,36 +19,24 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut
from eos.effectHandlerHelpers import HandledItem from eos.effectHandlerHelpers import HandledItem
import eos.db
class Mode(ItemAttrShortcut, HandledItem): class Mode(ItemAttrShortcut, HandledItem):
def __init__(self, item): def __init__(self, item):
if item.group.name != "Ship Modifiers":
raise ValueError('Passed item "%s" (category: (%s)) is not a Ship Modifier'%(item.name, item.category.name))
self.__item = item self.__item = item
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
if not isinstance(item, int):
self.__buildOriginal()
def __fetchItemInfo(self):
import eos.db
self.__item = eos.db.getItem(self.__item)
self.__buildOriginal()
def __buildOriginal(self):
self.__itemModifiedAttributes.original = self.item.attributes self.__itemModifiedAttributes.original = self.item.attributes
@property @property
def item(self): def item(self):
if isinstance(self.__item, int):
self.__fetchItemInfo()
return self.__item return self.__item
@property @property
def itemModifiedAttributes(self): def itemModifiedAttributes(self):
if isinstance(self.__item, int):
self.__fetchItemInfo()
return self.__itemModifiedAttributes return self.__itemModifiedAttributes
# @todo: rework to fit only on t3 dessy # @todo: rework to fit only on t3 dessy

View File

@@ -23,6 +23,10 @@ from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, C
from eos.effectHandlerHelpers import HandledItem, HandledCharge from eos.effectHandlerHelpers import HandledItem, HandledCharge
from eos.enum import Enum from eos.enum import Enum
from eos.mathUtils import floorFloat from eos.mathUtils import floorFloat
import eos.db
import logging
logger = logging.getLogger(__name__)
class State(Enum): class State(Enum):
OFFLINE = -1 OFFLINE = -1
@@ -49,95 +53,78 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
MINING_ATTRIBUTES = ("miningAmount", ) MINING_ATTRIBUTES = ("miningAmount", )
def __init__(self, item): def __init__(self, item):
self.__item = item if item != None else 0 """Initialize a module from the program"""
self.__item = item
if item is not None and self.isInvalid:
raise ValueError("Passed item is not a Module")
self.__charge = None
self.itemID = item.ID if item is not None else None self.itemID = item.ID if item is not None else None
self.__charge = 0
self.projected = False self.projected = False
self.state = State.ONLINE self.state = State.ONLINE
self.build()
@reconstructor
def init(self):
"""Initialize a module from the database and validate"""
self.__item = None
self.__charge = None
# we need this early if module is invalid and returns early
self.__slot = self.dummySlot
if self.itemID:
self.__item = eos.db.getItem(self.itemID)
if self.__item is None:
logger.error("Item (id: %d) does not exist", self.itemID)
return
if self.isInvalid:
logger.error("Item (id: %d) is not a Module", self.itemID)
return
if self.chargeID:
self.__charge = eos.db.getItem(self.chargeID)
self.build()
def build(self):
""" Builds internal module variables from both init's """
if self.__charge and self.__charge.category.name != "Charge":
self.__charge = None
self.__dps = None self.__dps = None
self.__miningyield = None self.__miningyield = None
self.__volley = None self.__volley = None
self.__reloadTime = None self.__reloadTime = None
self.__reloadForce = None self.__reloadForce = None
self.__chargeCycles = None self.__chargeCycles = None
self.__hardpoint = Hardpoint.NONE
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__slot = None
if item != None:
self.__itemModifiedAttributes.original = item.attributes
self.__hardpoint = self.__calculateHardpoint(item)
self.__slot = self.__calculateSlot(item)
self.__chargeModifiedAttributes = ModifiedAttributeDict() self.__chargeModifiedAttributes = ModifiedAttributeDict()
self.__slot = self.dummySlot # defaults to None
@reconstructor if self.__item:
def init(self): self.__itemModifiedAttributes.original = self.__item.attributes
if self.dummySlot is None: self.__hardpoint = self.__calculateHardpoint(self.__item)
self.__item = None self.__slot = self.__calculateSlot(self.__item)
self.__charge = None if self.__charge:
self.__volley = None self.__chargeModifiedAttributes.original = self.__charge.attributes
self.__dps = None
self.__miningyield = None
self.__reloadTime = None
self.__reloadForce = None
self.__chargeCycles = None
else:
self.__slot = self.dummySlot
self.__item = 0
self.__charge = 0
self.__dps = 0
self.__miningyield = 0
self.__volley = 0
self.__reloadTime = 0
self.__reloadForce = None
self.__chargeCycles = 0
self.__hardpoint = Hardpoint.NONE
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__chargeModifiedAttributes = ModifiedAttributeDict()
def __fetchItemInfo(self):
import eos.db
item = eos.db.getItem(self.itemID)
self.__item = item
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = item.attributes
self.__hardpoint = self.__calculateHardpoint(item)
self.__slot = self.__calculateSlot(item)
def __fetchChargeInfo(self):
self.__chargeModifiedAttributes = ModifiedAttributeDict()
if self.chargeID is not None:
import eos.db
charge = eos.db.getItem(self.chargeID)
self.__charge = charge
self.__chargeModifiedAttributes.original = charge.attributes
else:
self.__charge = 0
@classmethod @classmethod
def buildEmpty(cls, slot): def buildEmpty(cls, slot):
empty = Module(None) empty = Module(None)
empty.__slot = slot empty.__slot = slot
empty.__hardpoint = Hardpoint.NONE
empty.__item = 0
empty.__charge = 0
empty.dummySlot = slot empty.dummySlot = slot
empty.__itemModifiedAttributes = ModifiedAttributeDict()
empty.__chargeModifiedAttributes = ModifiedAttributeDict()
return empty return empty
@classmethod @classmethod
def buildRack(cls, slot): def buildRack(cls, slot):
empty = Rack(None) empty = Rack(None)
empty.__slot = slot empty.__slot = slot
empty.__hardpoint = Hardpoint.NONE
empty.__item = 0
empty.__charge = 0
empty.dummySlot = slot empty.dummySlot = slot
empty.__itemModifiedAttributes = ModifiedAttributeDict()
empty.__chargeModifiedAttributes = ModifiedAttributeDict()
return empty return empty
@property @property
@@ -146,11 +133,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@property @property
def hardpoint(self): def hardpoint(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__hardpoint return self.__hardpoint
@property
def isInvalid(self):
if self.isEmpty:
return False
return self.__item is None or (self.__item.category.name not in ("Module", "Subsystem") and self.__item.group.name != "Effect Beacon")
@property @property
def numCharges(self): def numCharges(self):
if self.charge is None: if self.charge is None:
@@ -263,38 +253,23 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@property @property
def slot(self): def slot(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__slot return self.__slot
@property @property
def itemModifiedAttributes(self): def itemModifiedAttributes(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__itemModifiedAttributes return self.__itemModifiedAttributes
@property @property
def chargeModifiedAttributes(self): def chargeModifiedAttributes(self):
if self.__charge is None:
self.__fetchChargeInfo()
return self.__chargeModifiedAttributes return self.__chargeModifiedAttributes
@property @property
def item(self): def item(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__item if self.__item != 0 else None return self.__item if self.__item != 0 else None
@property @property
def charge(self): def charge(self):
if self.__charge is None:
self.__fetchChargeInfo()
return self.__charge if self.__charge != 0 else None return self.__charge if self.__charge != 0 else None
@charge.setter @charge.setter
@@ -516,7 +491,6 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def getValidCharges(self): def getValidCharges(self):
validCharges = set() validCharges = set()
import eos.db
for i in range(5): for i in range(5):
itemChargeGroup = self.getModifiedItemAttr('chargeGroup' + str(i)) itemChargeGroup = self.getModifiedItemAttr('chargeGroup' + str(i))
if itemChargeGroup is not None: if itemChargeGroup is not None:

View File

@@ -26,6 +26,7 @@ class Price(object):
def __init__(self, typeID): def __init__(self, typeID):
self.typeID = typeID self.typeID = typeID
self.time = 0 self.time = 0
self.price = 0
self.failed = None self.failed = None
self.__item = None self.__item = None

View File

@@ -20,6 +20,10 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut
from eos.effectHandlerHelpers import HandledItem from eos.effectHandlerHelpers import HandledItem
from eos.saveddata.mode import Mode from eos.saveddata.mode import Mode
import eos.db
import logging
logger = logging.getLogger(__name__)
class Ship(ItemAttrShortcut, HandledItem): class Ship(ItemAttrShortcut, HandledItem):
def __init__(self, item): def __init__(self, item):
@@ -28,33 +32,18 @@ class Ship(ItemAttrShortcut, HandledItem):
raise ValueError('Passed item "%s" (category: (%s)) 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))
self.__item = item self.__item = item
self.__modeItems = self.__getModeItems()
self.__itemModifiedAttributes = ModifiedAttributeDict() self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__modeItems = self._getModeItems() self.__itemModifiedAttributes.original = self.item.attributes
if not isinstance(item, int):
self.__buildOriginal()
self.commandBonus = 0 self.commandBonus = 0
def __fetchItemInfo(self):
import eos.db
self.__item = eos.db.getItem(self.__item)
self.__buildOriginal()
def __buildOriginal(self):
self.__itemModifiedAttributes.original = self.item.attributes
@property @property
def item(self): def item(self):
if isinstance(self.__item, int):
self.__fetchItemInfo()
return self.__item return self.__item
@property @property
def itemModifiedAttributes(self): def itemModifiedAttributes(self):
if isinstance(self.__item, int):
self.__fetchItemInfo()
return self.__itemModifiedAttributes return self.__itemModifiedAttributes
def clear(self): def clear(self):
@@ -67,25 +56,16 @@ class Ship(ItemAttrShortcut, HandledItem):
if effect.runTime == runTime and effect.isType("passive"): if effect.runTime == runTime and effect.isType("passive"):
effect.handler(fit, self, ("ship",)) effect.handler(fit, self, ("ship",))
def checkModeItem(self, item): def validateModeItem(self, item):
""" """ Checks if provided item is a valid mode """
Checks if provided item is a valid mode.
If ship has modes, and current item is not valid, return forced mode
else if mode is valid, return Mode
else if ship does not have modes, return None
@todo: rename this
"""
items = self.__modeItems items = self.__modeItems
if items != None: if items is not None:
if item == None or item not in items: # if we have items, then we are in a tactical destroyer and must have a mode
# We have a tact dessy, but mode is None or not valid. Force new mode if item is None or item not in items:
# If provided item is invalid mode, force new one
return Mode(items[0]) return Mode(items[0])
elif item in items: return Mode(item)
# We have a valid mode
return Mode(item)
return None return None
@property @property
@@ -96,21 +76,17 @@ class Ship(ItemAttrShortcut, HandledItem):
def modes(self): def modes(self):
return [Mode(item) for item in self.__modeItems] if self.__modeItems else None return [Mode(item) for item in self.__modeItems] if self.__modeItems else None
def _getModeItems(self): def __getModeItems(self):
""" """
Returns a list of valid mode items for ship. Note that this returns the Returns a list of valid mode items for ship. Note that this returns the
valid Item objects, not the Mode objects. Returns None if not a valid Item objects, not the Mode objects. Returns None if not a
t3 dessy t3 dessy
""" """
# @todo: is there a better way to determine this that isn't hardcoded groupIDs? if self.item.group.name != "Tactical Destroyer":
if self.item.groupID != 1305:
return None return None
modeGroupID = 1306
import eos.db
items = [] items = []
g = eos.db.getGroup(modeGroupID, eager=("items.icon", "items.attributes")) g = eos.db.getGroup("Ship Modifiers", eager=("items.icon", "items.attributes"))
for item in g.items: for item in g.items:
# Rely on name detection because race is not reliable # Rely on name detection because race is not reliable
if item.name.lower().startswith(self.item.name.lower()): if item.name.lower().startswith(self.item.name.lower()):

View File

@@ -1,223 +0,0 @@
#===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
from eos.types import Slot, Fit, Module, State
import random
import copy
import math
import bisect
import itertools
import time
class SlotFill(object):
def __init__(self, original, modules, attributeWeights=None, propertyWeights=None, specificWeights=None, defaultState = State.ACTIVE):
self.original = original
self.attributeWeights = attributeWeights or {}
self.propertyWeights = propertyWeights or {}
self.specificWeights = specificWeights or []
self.state = State.ACTIVE
self.modules = map(self.__newModule, modules)
def __newModule(self, item):
m = Module(item)
m.state = self.state
return m
def __getMetaParent(self, item):
metaGroup = item.metaGroup
return item if metaGroup is None else metaGroup.parent
def fitness(self, fit, chromosome):
modList = fit.modules
modAttr = fit.ship.getModifiedItemAttr
modList.extend(chromosome)
fit.clear()
fit.calculateModifiedAttributes()
if not fit.fits:
del modList[-len(chromosome):]
return 0
weight = 0
for attr, value in self.attributeWeights.iteritems():
weight += modAttr(attr) * (value if value >= 0 else 1.0 / -value)
for prop, value in self.propertyWeights.iteritems():
weight += getattr(fit, prop) * (value if value >= 0 else 1.0 / -value)
for specific in self.specificWeights:
weight += specific(fit)
totalVars = (fit.ship.getModifiedItemAttr("powerOutput"),
fit.ship.getModifiedItemAttr("cpuOutput"),
fit.ship.getModifiedItemAttr('upgradeCapacity'))
usedVars = (fit.pgUsed, fit.cpuUsed, fit.calibrationUsed)
total = 0
used = 0
for tv, uv in zip(totalVars, usedVars):
if uv > tv:
del modList[-len(chromosome):]
return 0
del modList[-len(chromosome):]
return weight
def run(self, elite = 0.05, crossoverChance = 0.8, slotMutationChance = 0.5, typeMutationChance = 0.5):
#Use a copy of the original for all our calcs. We don't want to damage it
fit = copy.deepcopy(self.original)
fit.unfill()
#First of all, lets check the number of slots we got to play with
chromLength = -1
slotAmounts = {}
for type in Slot.getTypes():
slot = Slot.getValue(type)
amount = fit.getSlotsFree(slot)
if amount > 0:
slotAmounts[slot] = amount
chromLength += amount
if not slotAmounts:
#Nothing to do, joy
return
slotModules = {}
metaModules = {}
for slotType in slotAmounts:
slotModules[slotType] = modules = []
for module in self.modules:
#Store the variations of each base for ease and speed
metaParent = self.__getMetaParent(module.item)
metaList = metaModules.get(metaParent)
if metaList is None:
metaList = metaModules[metaParent] = []
metaList.append(module)
#Sort stuff by slotType for ease and speed
slot = module.slot
if slot in slotModules:
slotModules[slot].append(module)
for slotType, modules in slotModules.iteritems():
if len(modules) == 0:
chromLength -= slotAmounts[slotType]
del slotAmounts[slotType]
#Now, we need an initial set, first thing to do is decide how big that set will be
setSize = 10
#Grab some variables locally for performance improvements
rchoice = random.choice
rrandom = random.random
rrandint = random.randint
bbisect = bisect.bisect
ccopy = copy.copy
#Get our list for storage of our chromosomes
chromosomes = []
# Helpers
weigher = lambda chromosome: (self.fitness(fit, chromosome), chromosome)
keyer = lambda info: info[0]
eliteCutout = int(math.floor(setSize * (1 - elite)))
lastEl = setSize - 1
#Generate our initial set entirely randomly
#Subtelies to take in mind:
# * modules of the same slotType are kept together for easy cross-overing
state = self.state
for _ in xrange(setSize):
chrom = []
for type, amount in slotAmounts.iteritems():
for _ in xrange(amount):
chrom.append(rchoice(slotModules[type]))
chromosomes.append(weigher(chrom))
#Sort our initial set
chromosomes.sort(key=keyer)
currentGeneration = chromosomes
#Yield the best result from our initial set, this is gonna be pretty bad
yield currentGeneration[lastEl]
#Setup's done, now we can actualy apply our genetic algorithm to optimize all this
while True:
moo = time.time()
#First thing we do, we're gonna be elitair
#Grab the top x%, we'll put em in the next generation
nextGeneration = []
for i in xrange(lastEl, eliteCutout - 1, -1):
nextGeneration.append(currentGeneration[i])
#Figure out our ratios to do our roulette wheel
fitnessList = map(keyer, currentGeneration)
totalFitness = float(sum(fitnessList))
curr = 0
ratios = []
for fitness in fitnessList:
curr += fitness
ratios.append(curr / (totalFitness or 1))
t = 0
#Do our pairing
for _ in xrange(0, eliteCutout):
# Crossover chance
mother = currentGeneration[bbisect(ratios, rrandom())][1]
father = currentGeneration[bbisect(ratios, rrandom())][1]
if rrandom() <= crossoverChance:
crosspoint = rrandint(0, chromLength)
luke = mother[:crosspoint] + father[crosspoint:]
else:
luke = father
#Chance for slot mutation
if rrandom() <= slotMutationChance:
target = rrandint(0, chromLength)
mod = luke[target]
luke[target] = rchoice(slotModules[mod.slot])
if rrandom() <= typeMutationChance:
#Mutation of an item to another one of the same type
target = rrandint(0, chromLength)
mod = luke[target]
vars = metaModules[self.__getMetaParent(mod.item)]
luke[target] = rchoice(vars)
tt = time.time()
nextGeneration.append(weigher(luke))
t += time.time() - tt
print "time spent weighing: ", t
nextGeneration.sort(key=keyer)
currentGeneration = nextGeneration
print "total time spent this iteration:", time.time() - moo
yield currentGeneration[lastEl]

View File

@@ -71,10 +71,23 @@ def getBitmap(name,location):
# print "#BMPs:%d - Current took: %.8f" % (cachedBitmapsCount,time.clock() - start) # print "#BMPs:%d - Current took: %.8f" % (cachedBitmapsCount,time.clock() - start)
return bmp return bmp
def stripPath(fname):
"""
Here we extract 'core' of icon name. Path and
extension are sometimes specified in database
but we don't need them.
"""
# Path before the icon file name
fname = fname.split('/')[-1]
# Extension
fname = fname.rsplit('.', 1)[0]
return fname
def getImage(name, location): def getImage(name, location):
if location in locationMap: if location in locationMap:
if location == "pack": if location == "pack":
location = locationMap[location] location = locationMap[location]
name = stripPath(name)
filename = "icon{0}.png".format(name) filename = "icon{0}.png".format(name)
path = os.path.join(location, filename) path = os.path.join(location, filename)
else: else:

View File

@@ -17,5 +17,6 @@ __all__ = [
#"changeAffectingSkills", #"changeAffectingSkills",
"tacticalMode", "tacticalMode",
"targetResists", "targetResists",
"priceClear" "priceClear",
"amount",
] ]

View File

@@ -0,0 +1,76 @@
from gui.contextMenu import ContextMenu
from gui.itemStats import ItemStatsDialog
import eos.types
import gui.mainFrame
import service
import gui.globalEvents as GE
import wx
class ChangeAmount(ContextMenu):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, srcContext, selection):
return srcContext in ("cargoItem","projectedFit")
def getText(self, itmContext, selection):
print selection
return "Change {0} Quantity".format(itmContext)
def activate(self, fullContext, selection, i):
srcContext = fullContext[0]
dlg = AmountChanger(self.mainFrame, selection[0], srcContext)
dlg.ShowModal()
dlg.Destroy()
ChangeAmount.register()
class AmountChanger(wx.Dialog):
def __init__(self, parent, thing, context):
wx.Dialog.__init__(self, parent, title="Select Amount", size=wx.Size(220, 60))
self.thing = thing
self.context = context
bSizer1 = wx.BoxSizer(wx.HORIZONTAL)
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
bSizer1.Add(self.input, 1, wx.ALL, 5)
self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.change)
self.button = wx.Button(self, wx.ID_OK, u"Done")
bSizer1.Add(self.button, 0, wx.ALL, 5)
self.SetSizer(bSizer1)
self.Layout()
self.Centre(wx.BOTH)
self.button.Bind(wx.EVT_BUTTON, self.change)
def change(self, event):
sFit = service.Fit.getInstance()
mainFrame = gui.mainFrame.MainFrame.getInstance()
fitID = mainFrame.getActiveFit()
if isinstance(self.thing, eos.types.Cargo):
sFit.addCargo(fitID, self.thing.item.ID, int(self.input.GetLineText(0)), replace=True)
elif isinstance(self.thing, eos.types.Fit):
sFit.changeAmount(fitID, self.thing, int(self.input.GetLineText(0)))
wx.PostEvent(mainFrame, GE.FitChanged(fitID=fitID))
event.Skip()
self.Destroy()
## checks to make sure it's valid number
def onChar(self, event):
key = event.GetKeyCode()
acceptable_characters = "1234567890"
acceptable_keycode = [3, 22, 13, 8, 127] # modifiers like delete, copy, paste
if key in acceptable_keycode or key >= 255 or (key < 255 and chr(key) in acceptable_characters):
event.Skip()
return
else:
return False

View File

@@ -29,67 +29,3 @@ class Cargo(ContextMenu):
wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID)) wx.PostEvent(self.mainFrame, GE.FitChanged(fitID=fitID))
Cargo.register() Cargo.register()
class CargoAmount(ContextMenu):
def __init__(self):
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def display(self, srcContext, selection):
return srcContext in ("cargoItem",) and selection[0].amount >= 0
def getText(self, itmContext, selection):
return "Change {0} Quantity".format(itmContext)
def activate(self, fullContext, selection, i):
srcContext = fullContext[0]
dlg = CargoChanger(self.mainFrame, selection[0], srcContext)
dlg.ShowModal()
dlg.Destroy()
CargoAmount.register()
class CargoChanger(wx.Dialog):
def __init__(self, parent, cargo, context):
wx.Dialog.__init__(self, parent, title="Select Amount", size=wx.Size(220, 60))
self.cargo = cargo
self.context = context
bSizer1 = wx.BoxSizer(wx.HORIZONTAL)
self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER)
bSizer1.Add(self.input, 1, wx.ALL, 5)
self.input.Bind(wx.EVT_CHAR, self.onChar)
self.input.Bind(wx.EVT_TEXT_ENTER, self.change)
self.button = wx.Button(self, wx.ID_OK, u"Done")
bSizer1.Add(self.button, 0, wx.ALL, 5)
self.SetSizer(bSizer1)
self.Layout()
self.Centre(wx.BOTH)
self.button.Bind(wx.EVT_BUTTON, self.change)
def change(self, event):
sFit = service.Fit.getInstance()
mainFrame = gui.mainFrame.MainFrame.getInstance()
fitID = mainFrame.getActiveFit()
sFit.addCargo(fitID, self.cargo.item.ID, int(self.input.GetLineText(0)), replace=True)
wx.PostEvent(mainFrame, GE.FitChanged(fitID=fitID))
event.Skip()
self.Destroy()
## checks to make sure it's valid number
def onChar(self, event):
key = event.GetKeyCode()
acceptable_characters = "1234567890"
acceptable_keycode = [3, 22, 13, 8, 127] # modifiers like delete, copy, paste
if key in acceptable_keycode or key >= 255 or (key < 255 and chr(key) in acceptable_characters):
event.Skip()
return
else:
return False

View File

@@ -56,6 +56,9 @@ class PFGeneralPref ( PreferenceView):
self.cbShowTooltip = wx.CheckBox( panel, wx.ID_ANY, u"Show tab tooltips", wx.DefaultPosition, wx.DefaultSize, 0 ) self.cbShowTooltip = wx.CheckBox( panel, wx.ID_ANY, u"Show tab tooltips", wx.DefaultPosition, wx.DefaultSize, 0 )
mainSizer.Add( self.cbShowTooltip, 0, wx.ALL|wx.EXPAND, 5 ) mainSizer.Add( self.cbShowTooltip, 0, wx.ALL|wx.EXPAND, 5 )
self.cbMarketShortcuts = wx.CheckBox( panel, wx.ID_ANY, u"Show market shortcuts", wx.DefaultPosition, wx.DefaultSize, 0 )
mainSizer.Add( self.cbMarketShortcuts, 0, wx.ALL|wx.EXPAND, 5 )
defCharSizer = wx.BoxSizer( wx.HORIZONTAL ) defCharSizer = wx.BoxSizer( wx.HORIZONTAL )
self.sFit = service.Fit.getInstance() self.sFit = service.Fit.getInstance()
@@ -69,6 +72,7 @@ class PFGeneralPref ( PreferenceView):
self.cbCompactSkills.SetValue(self.sFit.serviceFittingOptions["compactSkills"] or False) self.cbCompactSkills.SetValue(self.sFit.serviceFittingOptions["compactSkills"] or False)
self.cbReopenFits.SetValue(self.openFitsSettings["enabled"]) self.cbReopenFits.SetValue(self.openFitsSettings["enabled"])
self.cbShowTooltip.SetValue(self.sFit.serviceFittingOptions["showTooltip"] or False) self.cbShowTooltip.SetValue(self.sFit.serviceFittingOptions["showTooltip"] or False)
self.cbMarketShortcuts.SetValue(self.sFit.serviceFittingOptions["showMarketShortcuts"] or False)
self.cbGlobalChar.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalCharStateChange) self.cbGlobalChar.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalCharStateChange)
self.cbGlobalDmgPattern.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalDmgPatternStateChange) self.cbGlobalDmgPattern.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalDmgPatternStateChange)
@@ -79,6 +83,7 @@ class PFGeneralPref ( PreferenceView):
self.cbCompactSkills.Bind(wx.EVT_CHECKBOX, self.onCBCompactSkills) self.cbCompactSkills.Bind(wx.EVT_CHECKBOX, self.onCBCompactSkills)
self.cbReopenFits.Bind(wx.EVT_CHECKBOX, self.onCBReopenFits) self.cbReopenFits.Bind(wx.EVT_CHECKBOX, self.onCBReopenFits)
self.cbShowTooltip.Bind(wx.EVT_CHECKBOX, self.onCBShowTooltip) self.cbShowTooltip.Bind(wx.EVT_CHECKBOX, self.onCBShowTooltip)
self.cbMarketShortcuts.Bind(wx.EVT_CHECKBOX, self.onCBShowShortcuts)
self.cbRackLabels.Enable(self.sFit.serviceFittingOptions["rackSlots"] or False) self.cbRackLabels.Enable(self.sFit.serviceFittingOptions["rackSlots"] or False)
@@ -135,6 +140,9 @@ class PFGeneralPref ( PreferenceView):
def onCBShowTooltip(self, event): def onCBShowTooltip(self, event):
self.sFit.serviceFittingOptions["showTooltip"] = self.cbShowTooltip.GetValue() self.sFit.serviceFittingOptions["showTooltip"] = self.cbShowTooltip.GetValue()
def onCBShowShortcuts(self, event):
self.sFit.serviceFittingOptions["showMarketShortcuts"] = self.cbMarketShortcuts.GetValue()
def getImage(self): def getImage(self):
return bitmapLoader.getBitmap("prefs_settings", "icons") return bitmapLoader.getBitmap("prefs_settings", "icons")

View File

@@ -30,37 +30,13 @@ class PriceViewFull(StatsView):
def __init__(self, parent): def __init__(self, parent):
StatsView.__init__(self) StatsView.__init__(self)
self.parent = parent self.parent = parent
self._timerId = wx.NewId()
self._timer = None
self.parent.Bind(wx.EVT_TIMER, self.OnTimer)
self._timerRunsBeforeUpdate = 60
self._timerRuns = 0
self._timerIdUpdate = wx.NewId()
self._timerUpdate = None
self._cachedShip = 0 self._cachedShip = 0
self._cachedFittings = 0 self._cachedFittings = 0
self._cachedTotal = 0 self._cachedTotal = 0
def OnTimer(self, event):
if self._timerId == event.GetId():
if self._timerRuns >= self._timerRunsBeforeUpdate:
self._timerRuns = 0
self._timer.Stop()
self.refreshPanel(self.fit)
else:
self.labelEMStatus.SetLabel("Prices update retry in: %d seconds" %(self._timerRunsBeforeUpdate - self._timerRuns))
self._timerRuns += 1
if self._timerIdUpdate == event.GetId():
self._timerUpdate.Stop()
self.labelEMStatus.SetLabel("")
def getHeaderText(self, fit): def getHeaderText(self, fit):
return "Price" return "Price"
def getTextExtentW(self, text):
width, height = self.parent.GetTextExtent(text)
return width
def populatePanel(self, contentPanel, headerPanel): def populatePanel(self, contentPanel, headerPanel):
contentSizer = contentPanel.GetSizer() contentSizer = contentPanel.GetSizer()
self.panel = contentPanel self.panel = contentPanel
@@ -111,22 +87,11 @@ class PriceViewFull(StatsView):
for cargo in fit.cargo: for cargo in fit.cargo:
for _ in xrange(cargo.amount): for _ in xrange(cargo.amount):
typeIDs.append(cargo.itemID) typeIDs.append(cargo.itemID)
if self._timer:
if self._timer.IsRunning():
self._timer.Stop()
sMkt = service.Market.getInstance() sMkt = service.Market.getInstance()
sMkt.getPrices(typeIDs, self.processPrices) sMkt.getPrices(typeIDs, self.processPrices)
self.labelEMStatus.SetLabel("Updating prices...") self.labelEMStatus.SetLabel("Updating prices...")
if not self._timerUpdate:
self._timerUpdate = wx.Timer(self.parent, self._timerIdUpdate)
if self._timerUpdate:
if not self._timerUpdate.IsRunning():
self._timerUpdate.Start(1000)
else: else:
if self._timer:
if self._timer.IsRunning():
self._timer.Stop()
self.labelEMStatus.SetLabel("") self.labelEMStatus.SetLabel("")
self.labelPriceShip.SetLabel("0.0 ISK") self.labelPriceShip.SetLabel("0.0 ISK")
self.labelPriceFittings.SetLabel("0.0 ISK") self.labelPriceFittings.SetLabel("0.0 ISK")
@@ -136,20 +101,10 @@ class PriceViewFull(StatsView):
def processPrices(self, prices): def processPrices(self, prices):
shipPrice = prices[0].price shipPrice = prices[0].price
if shipPrice == None:
if not self._timer:
self._timer = wx.Timer(self.parent, self._timerId)
self._timer.Start(1000)
self._timerRuns = 0
else:
if self._timer:
self._timer.Stop()
self.labelEMStatus.SetLabel("")
if shipPrice == None:
shipPrice = 0
modPrice = sum(map(lambda p: p.price or 0, prices[1:])) modPrice = sum(map(lambda p: p.price or 0, prices[1:]))
self.labelEMStatus.SetLabel("")
if self._cachedShip != shipPrice: if self._cachedShip != shipPrice:
self.labelPriceShip.SetLabel("%s ISK" % formatAmount(shipPrice, 3, 3, 9, currency=True)) self.labelPriceShip.SetLabel("%s ISK" % formatAmount(shipPrice, 3, 3, 9, currency=True))
self.labelPriceShip.SetToolTip(wx.ToolTip(locale.format('%.2f', shipPrice, 1))) self.labelPriceShip.SetToolTip(wx.ToolTip(locale.format('%.2f', shipPrice, 1)))

View File

@@ -197,8 +197,9 @@ class ResistancesViewFull(StatsView):
lbl = getattr(self, "labelResistance%sEhp" % tankType.capitalize()) lbl = getattr(self, "labelResistance%sEhp" % tankType.capitalize())
if ehp is not None: if ehp is not None:
total += ehp[tankType] total += ehp[tankType]
rrFactor = fit.ehp[tankType] / fit.hp[tankType]
lbl.SetLabel(formatAmount(ehp[tankType], 3, 0, 9)) lbl.SetLabel(formatAmount(ehp[tankType], 3, 0, 9))
lbl.SetToolTip(wx.ToolTip("%s: %d" % (tankType.capitalize(), ehp[tankType]))) lbl.SetToolTip(wx.ToolTip("%s: %d\nResist Multiplier: x%.2f" % (tankType.capitalize(), ehp[tankType], rrFactor)))
else: else:
lbl.SetLabel("0") lbl.SetLabel("0")

View File

@@ -21,6 +21,8 @@
from gui import builtinViewColumns from gui import builtinViewColumns
from gui.viewColumn import ViewColumn from gui.viewColumn import ViewColumn
from gui import bitmapLoader from gui import bitmapLoader
import gui.mainFrame
import wx import wx
from eos.types import Drone, Cargo, Fit, Module, Slot, Rack from eos.types import Drone, Cargo, Fit, Module, Slot, Rack
import service import service
@@ -29,6 +31,7 @@ class BaseName(ViewColumn):
name = "Base Name" name = "Base Name"
def __init__(self, fittingView, params): def __init__(self, fittingView, params):
ViewColumn.__init__(self, fittingView) ViewColumn.__init__(self, fittingView)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.columnText = "Name" self.columnText = "Name"
self.shipImage = fittingView.imageList.GetImageIndex("ship_small", "icons") self.shipImage = fittingView.imageList.GetImageIndex("ship_small", "icons")
self.mask = wx.LIST_MASK_TEXT self.mask = wx.LIST_MASK_TEXT
@@ -39,7 +42,8 @@ class BaseName(ViewColumn):
elif isinstance(stuff, Cargo): elif isinstance(stuff, Cargo):
return "%dx %s" % (stuff.amount, stuff.item.name) return "%dx %s" % (stuff.amount, stuff.item.name)
elif isinstance(stuff, Fit): elif isinstance(stuff, Fit):
return "%s (%s)" % (stuff.name, stuff.ship.item.name) fitID = self.mainFrame.getActiveFit()
return "%dx %s (%s)" % (stuff.getProjectionInfo(fitID).amount, stuff.name, stuff.ship.item.name)
elif isinstance(stuff, Rack): elif isinstance(stuff, Rack):
if service.Fit.getInstance().serviceFittingOptions["rackLabels"]: if service.Fit.getInstance().serviceFittingOptions["rackLabels"]:
if stuff.slot == Slot.MODE: if stuff.slot == Slot.MODE:
@@ -55,6 +59,15 @@ class BaseName(ViewColumn):
return stuff.item.name return stuff.item.name
else: else:
item = getattr(stuff, "item", stuff) item = getattr(stuff, "item", stuff)
if service.Fit.getInstance().serviceFittingOptions["showMarketShortcuts"]:
marketShortcut = getattr(item, "marketShortcut", None)
if marketShortcut:
# use unicode subscript to display shortcut value
shortcut = unichr(marketShortcut+8320)+u" "
return shortcut+item.name
return item.name return item.name
BaseName.register() BaseName.register()

View File

@@ -33,13 +33,13 @@ class Price(ViewColumn):
self.imageId = fittingView.imageList.GetImageIndex("totalPrice_small", "icons") self.imageId = fittingView.imageList.GetImageIndex("totalPrice_small", "icons")
def getText(self, stuff): def getText(self, stuff):
if stuff.item is None: if stuff.item is None or stuff.item.group.name == "Ship Modifiers":
return "" return ""
sMkt = service.Market.getInstance() sMkt = service.Market.getInstance()
price = sMkt.getPriceNow(stuff.item.ID) price = sMkt.getPriceNow(stuff.item.ID)
if not price or not price.price: if not price or not price.price or not price.isValid:
return False return False
price = price.price # Set new price variable with what we need price = price.price # Set new price variable with what we need
@@ -50,12 +50,17 @@ class Price(ViewColumn):
return formatAmount(price, 3, 3, 9, currency=True) return formatAmount(price, 3, 3, 9, currency=True)
def delayedText(self, mod, display, colItem): def delayedText(self, mod, display, colItem):
def callback(requests): sMkt = service.Market.getInstance()
price = requests[0].price def callback(item):
colItem.SetText(formatAmount(price, 3, 3, 9, currency=True) if price else "") price = sMkt.getPriceNow(item.ID)
text = formatAmount(price.price, 3, 3, 9, currency=True) if price.price else ""
if price.failed: text += " (!)"
colItem.SetText(text)
display.SetItem(colItem) display.SetItem(colItem)
service.Market.getInstance().getPrices([mod.item.ID], callback)
sMkt.waitForPrice(mod.item, callback)
def getImageId(self, mod): def getImageId(self, mod):
return -1 return -1

View File

@@ -19,27 +19,21 @@
from gui.viewColumn import ViewColumn from gui.viewColumn import ViewColumn
from gui import bitmapLoader from gui import bitmapLoader
import gui.mainFrame
import wx import wx
from eos.types import Drone, Module, Rack from eos.types import Drone, Module, Rack, Fit
from eos.types import State as State_ from eos.types import State as State_
class State(ViewColumn): class State(ViewColumn):
name = "State" name = "State"
def __init__(self, fittingView, params): def __init__(self, fittingView, params):
ViewColumn.__init__(self, fittingView) ViewColumn.__init__(self, fittingView)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.resizable = False self.resizable = False
self.size = 16 self.size = 16
self.maxsize = self.size self.maxsize = self.size
self.mask = wx.LIST_MASK_IMAGE self.mask = wx.LIST_MASK_IMAGE
for name, state in (("checked", wx.CONTROL_CHECKED), ("unchecked", 0)):
bitmap = wx.EmptyBitmap(16, 16)
dc = wx.MemoryDC()
dc.SelectObject(bitmap)
dc.SetBackground(wx.TheBrushList.FindOrCreateBrush(fittingView.GetBackgroundColour(), wx.SOLID))
dc.Clear()
wx.RendererNative.Get().DrawCheckBox(fittingView, dc, wx.Rect(0, 0, 16, 16), state)
dc.Destroy()
setattr(self, "%sId" % name, fittingView.imageList.Add(bitmap))
def getText(self, mod): def getText(self, mod):
return "" return ""
@@ -49,8 +43,14 @@ class State(ViewColumn):
return State_.getName(mod.state).title() return State_.getName(mod.state).title()
def getImageId(self, stuff): def getImageId(self, stuff):
generic_active = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(1).lower(), "icons")
generic_inactive = self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(-1).lower(), "icons")
if isinstance(stuff, Drone): if isinstance(stuff, Drone):
return self.checkedId if stuff.amountActive > 0 else self.uncheckedId if stuff.amountActive > 0:
return generic_active
else:
return generic_inactive
elif isinstance(stuff, Rack): elif isinstance(stuff, Rack):
return -1 return -1
elif isinstance(stuff, Module): elif isinstance(stuff, Module):
@@ -58,11 +58,21 @@ class State(ViewColumn):
return -1 return -1
else: else:
return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(stuff.state).lower(), "icons") return self.fittingView.imageList.GetImageIndex("state_%s_small" % State_.getName(stuff.state).lower(), "icons")
elif isinstance(stuff, Fit):
fitID = self.mainFrame.getActiveFit()
projectionInfo = stuff.getProjectionInfo(fitID)
if projectionInfo is None:
return -1
if projectionInfo.active:
return generic_active
return generic_inactive
else: else:
active = getattr(stuff, "active", None) active = getattr(stuff, "active", None)
if active is None: if active is None:
return -1 return -1
else: if active:
return self.checkedId if active else self.uncheckedId return generic_active
return generic_inactive
State.register() State.register()

View File

@@ -91,10 +91,10 @@ class CharacterEditor(wx.Frame):
self.viewsNBContainer = wx.Notebook(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0) self.viewsNBContainer = wx.Notebook(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, 0)
self.sview = SkillTreeView(self.viewsNBContainer) self.sview = SkillTreeView(self.viewsNBContainer)
self.iview = ImplantsTreeView(self.viewsNBContainer) #self.iview = ImplantsTreeView(self.viewsNBContainer)
#======================================================================= #=======================================================================
# RC2 # RC2
self.iview.Show(False) #self.iview.Show(False)
#======================================================================= #=======================================================================
self.aview = APIView(self.viewsNBContainer) self.aview = APIView(self.viewsNBContainer)
@@ -147,6 +147,7 @@ class CharacterEditor(wx.Frame):
def restrict(self): def restrict(self):
self.btnRename.Enable(False) self.btnRename.Enable(False)
self.btnDelete.Enable(False) self.btnDelete.Enable(False)
self.aview.stDisabledTip.Show(True)
self.aview.inputID.Enable(False) self.aview.inputID.Enable(False)
self.aview.inputKey.Enable(False) self.aview.inputKey.Enable(False)
self.aview.charChoice.Enable(False) self.aview.charChoice.Enable(False)
@@ -158,6 +159,7 @@ class CharacterEditor(wx.Frame):
def unrestrict(self): def unrestrict(self):
self.btnRename.Enable(True) self.btnRename.Enable(True)
self.btnDelete.Enable(True) self.btnDelete.Enable(True)
self.aview.stDisabledTip.Show(False)
self.aview.inputID.Enable(True) self.aview.inputID.Enable(True)
self.aview.inputKey.Enable(True) self.aview.inputKey.Enable(True)
self.aview.btnFetchCharList.Enable(True) self.aview.btnFetchCharList.Enable(True)
@@ -272,6 +274,7 @@ class CharacterEditor(wx.Frame):
class SkillTreeView (wx.Panel): class SkillTreeView (wx.Panel):
def __init__(self, parent): def __init__(self, parent):
wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Size(500, 300), style=wx.TAB_TRAVERSAL) wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Size(500, 300), style=wx.TAB_TRAVERSAL)
self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
pmainSizer = wx.BoxSizer(wx.VERTICAL) pmainSizer = wx.BoxSizer(wx.VERTICAL)
@@ -531,12 +534,23 @@ class APIView (wx.Panel):
def __init__(self, parent): def __init__(self, parent):
wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Size(500, 300), style=wx.TAB_TRAVERSAL) wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Size(500, 300), style=wx.TAB_TRAVERSAL)
self.Parent.Parent.Bind(GE.CHAR_CHANGED, self.charChanged) self.Parent.Parent.Bind(GE.CHAR_CHANGED, self.charChanged)
self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
self.apiUrlCreatePredefined = u"https://community.eveonline.com/support/api-key/CreatePredefined?accessMask=8" self.apiUrlCreatePredefined = u"https://community.eveonline.com/support/api-key/CreatePredefined?accessMask=8"
self.apiUrlKeyList = u"https://community.eveonline.com/support/api-key/" self.apiUrlKeyList = u"https://community.eveonline.com/support/api-key/"
pmainSizer = wx.BoxSizer(wx.VERTICAL) pmainSizer = wx.BoxSizer(wx.VERTICAL)
hintSizer = wx.BoxSizer( wx.HORIZONTAL )
hintSizer.AddStretchSpacer()
self.stDisabledTip = wx.StaticText( self, wx.ID_ANY, u"You cannot add API Details for All 0 and All 5 characters.\n"
u"Please select another character or make a new one.", style=wx.ALIGN_CENTER )
self.stDisabledTip.Wrap( -1 )
hintSizer.Add( self.stDisabledTip, 0, wx.TOP | wx.BOTTOM, 10 )
hintSizer.AddStretchSpacer()
pmainSizer.Add(hintSizer, 0, wx.EXPAND, 5)
fgSizerInput = wx.FlexGridSizer(3, 2, 0, 0) fgSizerInput = wx.FlexGridSizer(3, 2, 0, 0)
fgSizerInput.AddGrowableCol(1) fgSizerInput.AddGrowableCol(1)
fgSizerInput.SetFlexibleDirection(wx.BOTH) fgSizerInput.SetFlexibleDirection(wx.BOTH)

View File

@@ -250,7 +250,7 @@ class Display(wx.ListCtrl):
newText = col.getText(st) newText = col.getText(st)
if newText is False: if newText is False:
col.delayedText(st, self, colItem) col.delayedText(st, self, colItem)
newText = "" newText = u"\u21bb"
newImageId = col.getImageId(st) newImageId = col.getImageId(st)

View File

@@ -36,7 +36,8 @@ class DroneViewDrop(wx.PyDropTarget):
def OnData(self, x, y, t): def OnData(self, x, y, t):
if self.GetData(): if self.GetData():
self.dropFn(x, y, int(self.dropData.GetText())) data = self.dropData.GetText().split(':')
self.dropFn(x, y, data)
return t return t
class DroneView(d.Display): class DroneView(d.Display):
@@ -72,7 +73,7 @@ class DroneView(d.Display):
self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag) self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag)
self.SetDropTarget(DroneViewDrop(self.mergeDrones)) self.SetDropTarget(DroneViewDrop(self.handleDragDrop))
def OnLeaveWindow(self, event): def OnLeaveWindow(self, event):
self.SetToolTip(None) self.SetToolTip(None)
@@ -117,17 +118,27 @@ class DroneView(d.Display):
row = event.GetIndex() row = event.GetIndex()
if row != -1: if row != -1:
data = wx.PyTextDataObject() data = wx.PyTextDataObject()
data.SetText(str(self.GetItemData(row))) data.SetText("drone:"+str(row))
dropSource = wx.DropSource(self) dropSource = wx.DropSource(self)
dropSource.SetData(data) dropSource.SetData(data)
res = dropSource.DoDragDrop() res = dropSource.DoDragDrop()
def mergeDrones(self, x, y, itemID): def handleDragDrop(self, x, y, data):
srcRow = self.FindItemData(-1,itemID) '''
dstRow, _ = self.HitTest((x, y)) Handles dragging of items from various pyfa displays which support it
if srcRow != -1 and dstRow != -1:
self._merge(srcRow, dstRow) data is list with two indices:
data[0] is hard-coded str of originating source
data[1] is typeID or index of data we want to manipulate
'''
if data[0] == "drone": # we want to merge drones
srcRow = int(data[1])
dstRow, _ = self.HitTest((x, y))
if srcRow != -1 and dstRow != -1:
self._merge(srcRow, dstRow)
elif data[0] == "market":
wx.PostEvent(self.mainFrame, mb.ItemSelected(itemID=int(data[1])))
def _merge(self, src, dst): def _merge(self, src, dst):
sFit = service.Fit.getInstance() sFit = service.Fit.getInstance()

View File

@@ -39,7 +39,7 @@ import gui.globalEvents as GE
from gui import bitmapLoader from gui import bitmapLoader
from gui.mainMenuBar import MainMenuBar from gui.mainMenuBar import MainMenuBar
from gui.additionsPane import AdditionsPane from gui.additionsPane import AdditionsPane
from gui.marketBrowser import MarketBrowser from gui.marketBrowser import MarketBrowser, ItemSelected
from gui.multiSwitch import MultiSwitch from gui.multiSwitch import MultiSwitch
from gui.statsPane import StatsPane from gui.statsPane import StatsPane
from gui.shipBrowser import ShipBrowser, FitSelected, ImportSelected, Stage3Selected from gui.shipBrowser import ShipBrowser, FitSelected, ImportSelected, Stage3Selected
@@ -146,6 +146,7 @@ class MainFrame(wx.Frame):
self.marketBrowser = MarketBrowser(self.notebookBrowsers) self.marketBrowser = MarketBrowser(self.notebookBrowsers)
self.notebookBrowsers.AddPage(self.marketBrowser, "Market", tabImage = marketImg, showClose = False) self.notebookBrowsers.AddPage(self.marketBrowser, "Market", tabImage = marketImg, showClose = False)
self.marketBrowser.splitter.SetSashPosition(self.marketHeight)
self.shipBrowser = ShipBrowser(self.notebookBrowsers) self.shipBrowser = ShipBrowser(self.notebookBrowsers)
self.notebookBrowsers.AddPage(self.shipBrowser, "Ships", tabImage = shipBrowserImg, showClose = False) self.notebookBrowsers.AddPage(self.shipBrowser, "Ships", tabImage = shipBrowserImg, showClose = False)
@@ -160,7 +161,7 @@ class MainFrame(wx.Frame):
self.splitter.SplitVertically(self.notebookBrowsers, self.FitviewAdditionsPanel) self.splitter.SplitVertically(self.notebookBrowsers, self.FitviewAdditionsPanel)
self.splitter.SetMinimumPaneSize(204) self.splitter.SetMinimumPaneSize(204)
self.splitter.SetSashPosition(300) self.splitter.SetSashPosition(self.browserWidth)
cstatsSizer = wx.BoxSizer(wx.VERTICAL) cstatsSizer = wx.BoxSizer(wx.VERTICAL)
@@ -227,7 +228,7 @@ class MainFrame(wx.Frame):
def LoadMainFrameAttribs(self): def LoadMainFrameAttribs(self):
mainFrameDefaultAttribs = {"wnd_width": 1000, "wnd_height": 680, "wnd_maximized": False} mainFrameDefaultAttribs = {"wnd_width": 1000, "wnd_height": 680, "wnd_maximized": False, "browser_width": 300, "market_height": 0}
self.mainFrameAttribs = service.SettingsProvider.getInstance().getSettings("pyfaMainWindowAttribs", mainFrameDefaultAttribs) self.mainFrameAttribs = service.SettingsProvider.getInstance().getSettings("pyfaMainWindowAttribs", mainFrameDefaultAttribs)
if self.mainFrameAttribs["wnd_maximized"]: if self.mainFrameAttribs["wnd_maximized"]:
@@ -241,6 +242,9 @@ class MainFrame(wx.Frame):
self.SetSize((width, height)) self.SetSize((width, height))
self.SetMinSize((mainFrameDefaultAttribs["wnd_width"], mainFrameDefaultAttribs["wnd_height"])) self.SetMinSize((mainFrameDefaultAttribs["wnd_width"], mainFrameDefaultAttribs["wnd_height"]))
self.browserWidth = self.mainFrameAttribs["browser_width"]
self.marketHeight = self.mainFrameAttribs["market_height"]
def UpdateMainFrameAttribs(self): def UpdateMainFrameAttribs(self):
if self.IsIconized(): if self.IsIconized():
return return
@@ -250,6 +254,9 @@ class MainFrame(wx.Frame):
self.mainFrameAttribs["wnd_height"] = height self.mainFrameAttribs["wnd_height"] = height
self.mainFrameAttribs["wnd_maximized"] = self.IsMaximized() self.mainFrameAttribs["wnd_maximized"] = self.IsMaximized()
self.mainFrameAttribs["browser_width"] = self.notebookBrowsers.GetSize()[0]
self.mainFrameAttribs["market_height"] = self.marketBrowser.marketView.GetSize()[1]
def SetActiveStatsWindow(self, wnd): def SetActiveStatsWindow(self, wnd):
self.activeStatsWnd = wnd self.activeStatsWnd = wnd
@@ -423,12 +430,6 @@ class MainFrame(wx.Frame):
ctabnext = wx.NewId() ctabnext = wx.NewId()
ctabprev = wx.NewId() ctabprev = wx.NewId()
self.additionstab1 = wx.NewId()
self.additionstab2 = wx.NewId()
self.additionstab3 = wx.NewId()
self.additionstab4 = wx.NewId()
self.additionstab5 = wx.NewId()
# Close Page # Close Page
self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId) self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId)
self.Bind(wx.EVT_MENU, self.HAddPage, id = self.addPageId) self.Bind(wx.EVT_MENU, self.HAddPage, id = self.addPageId)
@@ -437,12 +438,6 @@ class MainFrame(wx.Frame):
self.Bind(wx.EVT_MENU, self.CTabNext, id = ctabnext) self.Bind(wx.EVT_MENU, self.CTabNext, id = ctabnext)
self.Bind(wx.EVT_MENU, self.CTabPrev, id = ctabprev) self.Bind(wx.EVT_MENU, self.CTabPrev, id = ctabprev)
self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab1)
self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab2)
self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab3)
self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab4)
self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id = self.additionstab5)
actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId), actb = [(wx.ACCEL_CTRL, ord('T'), self.addPageId),
(wx.ACCEL_CMD, ord('T'), self.addPageId), (wx.ACCEL_CMD, ord('T'), self.addPageId),
@@ -462,41 +457,43 @@ class MainFrame(wx.Frame):
(wx.ACCEL_CMD, wx.WXK_TAB, ctabnext), (wx.ACCEL_CMD, wx.WXK_TAB, ctabnext),
(wx.ACCEL_CMD | wx.ACCEL_SHIFT, wx.WXK_TAB, ctabprev), (wx.ACCEL_CMD | wx.ACCEL_SHIFT, wx.WXK_TAB, ctabprev),
# Ctrl+age(Up/Down) # Ctrl+Page(Up/Down)
(wx.ACCEL_CTRL, wx.WXK_PAGEDOWN, ctabnext), (wx.ACCEL_CTRL, wx.WXK_PAGEDOWN, ctabnext),
(wx.ACCEL_CTRL, wx.WXK_PAGEUP, ctabprev), (wx.ACCEL_CTRL, wx.WXK_PAGEUP, ctabprev),
(wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext), (wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext),
(wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev), (wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev)
(wx.ACCEL_CTRL, ord('1'), self.additionstab1),
(wx.ACCEL_CTRL, ord('2'), self.additionstab2),
(wx.ACCEL_CTRL, ord('3'), self.additionstab3),
(wx.ACCEL_CTRL, ord('4'), self.additionstab4),
(wx.ACCEL_CTRL, ord('5'), self.additionstab5),
(wx.ACCEL_CMD, ord('1'), self.additionstab1),
(wx.ACCEL_CMD, ord('2'), self.additionstab2),
(wx.ACCEL_CMD, ord('3'), self.additionstab3),
(wx.ACCEL_CMD, ord('4'), self.additionstab4),
(wx.ACCEL_CMD, ord('5'), self.additionstab5)
] ]
# Ctrl/Cmd+# for addition pane selection
self.additionsSelect = []
for i in range(0, self.additionsPane.notebook.GetPageCount()):
self.additionsSelect.append(wx.NewId())
self.Bind(wx.EVT_MENU, self.AdditionsTabSelect, id=self.additionsSelect[i])
actb.append((wx.ACCEL_CMD, i+49, self.additionsSelect[i]))
actb.append((wx.ACCEL_CTRL, i+49, self.additionsSelect[i]))
# Alt+1-9 for market item selection
self.itemSelect = []
for i in range(0, 9):
self.itemSelect.append(wx.NewId())
self.Bind(wx.EVT_MENU, self.ItemSelect, id = self.itemSelect[i])
actb.append((wx.ACCEL_ALT, i + 49, self.itemSelect[i]))
atable = wx.AcceleratorTable(actb) atable = wx.AcceleratorTable(actb)
self.SetAcceleratorTable(atable) self.SetAcceleratorTable(atable)
def AdditionsTabSelect(self, event): def AdditionsTabSelect(self, event):
selTab = None selTab = self.additionsSelect.index(event.GetId())
if event.GetId() == self.additionstab1:
selTab = 0 if selTab <= self.additionsPane.notebook.GetPageCount():
if event.GetId() == self.additionstab2:
selTab = 1
if event.GetId() == self.additionstab3:
selTab = 2
if event.GetId() == self.additionstab4:
selTab = 3
if event.GetId() == self.additionstab5:
selTab = 4
if selTab is not None:
self.additionsPane.notebook.SetSelection(selTab) self.additionsPane.notebook.SetSelection(selTab)
def ItemSelect(self, event):
selItem = self.itemSelect.index(event.GetId())
if selItem < len(self.marketBrowser.itemView.active):
wx.PostEvent(self, ItemSelected(itemID=self.marketBrowser.itemView.active[selItem].ID))
def CTabNext(self, event): def CTabNext(self, event):
self.fitMultiSwitch.NextPage() self.fitMultiSwitch.NextPage()

View File

@@ -438,6 +438,11 @@ class ItemView(d.Display):
self.metalvls = sMkt.directAttrRequest(items, attrs) self.metalvls = sMkt.directAttrRequest(items, attrs)
# Re-sort stuff # Re-sort stuff
items.sort(key=self.itemSort) items.sort(key=self.itemSort)
for i, item in enumerate(items[:9]):
# set shortcut info for first 9 modules
item.marketShortcut = i+1
d.Display.refresh(self, items) d.Display.refresh(self, items)
def makeReverseMetaMap(self): def makeReverseMetaMap(self):

View File

@@ -342,7 +342,6 @@ class ResistsEditorDlg(wx.Dialog):
self.patternChanged() self.patternChanged()
def showInput(self, bool): def showInput(self, bool):
print self.namePicker.IsShown(), bool
if bool and not self.namePicker.IsShown(): if bool and not self.namePicker.IsShown():
self.ccResists.Hide() self.ccResists.Hide()
self.namePicker.Show() self.namePicker.Show()

View File

@@ -939,11 +939,12 @@ class ShipBrowser(wx.Panel):
class PFStaticText(wx.Panel): class PFStaticText(wx.Panel):
def __init__(self, parent, label=wx.EmptyString): def __init__(self, parent, label=wx.EmptyString):
wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size = parent.GetSize()) wx.Panel.__init__ (self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size = parent.GetSize())
self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer = wx.BoxSizer(wx.VERTICAL)
text = wx.StaticText( self, wx.ID_ANY, label, wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_CENTRE ) text = wx.StaticText( self, wx.ID_ANY, label, wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_CENTRE )
text.Wrap( -1 ) text.Wrap( -1 )
mainSizer.Add( text, 1, wx.EXPAND|wx.TOP, 10 ) mainSizer.Add( text, 1, wx.ALL, 10 )
self.SetSizer(mainSizer) self.SetSizer(mainSizer)
self.Layout() self.Layout()
def GetType(self): def GetType(self):
@@ -1426,9 +1427,6 @@ class FitItem(SFItem.SFBrowserItem):
self.deleted = False self.deleted = False
# @todo: replace all getActiveFit() in class with this variable and test
self.activeFit = self.mainFrame.getActiveFit()
if shipID: if shipID:
self.shipBmp = bitmapLoader.getBitmap(str(shipID),"ships") self.shipBmp = bitmapLoader.getBitmap(str(shipID),"ships")
@@ -1441,19 +1439,6 @@ class FitItem(SFItem.SFBrowserItem):
# see GH issue #62 # see GH issue #62
if self.fitBooster is None: self.fitBooster = False if self.fitBooster is None: self.fitBooster = False
# access these by index based on toggle for booster fit
self.fitMenu = wx.Menu()
self.toggleItem = self.fitMenu.Append(-1, "Booster Fit", kind=wx.ITEM_CHECK)
self.fitMenu.Check(self.toggleItem.GetId(), self.fitBooster)
self.Bind(wx.EVT_MENU, self.OnPopupItemSelected, self.toggleItem)
if self.activeFit:
# If there is an active fit, get menu for setting individual boosters
self.fitMenu.AppendSeparator()
boosterMenu = self.mainFrame.additionsPane.gangPage.FitDNDPopupMenu
self.fitMenu.AppendMenu(wx.ID_ANY, 'Set Booster', boosterMenu)
self.boosterBmp = bitmapLoader.getBitmap("fleet_fc_small", "icons") self.boosterBmp = bitmapLoader.getBitmap("fleet_fc_small", "icons")
self.copyBmp = bitmapLoader.getBitmap("fit_add_small", "icons") self.copyBmp = bitmapLoader.getBitmap("fit_add_small", "icons")
self.renameBmp = bitmapLoader.getBitmap("fit_rename_small", "icons") self.renameBmp = bitmapLoader.getBitmap("fit_rename_small", "icons")
@@ -1530,14 +1515,12 @@ class FitItem(SFItem.SFBrowserItem):
self.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu) self.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu)
def OnPopupItemSelected(self, event): def OnToggleBooster(self, event):
''' Fires when fit menu item is selected '''
# currently only have one menu option (toggle booster)
sFit = service.Fit.getInstance() sFit = service.Fit.getInstance()
sFit.toggleBoostFit(self.fitID) sFit.toggleBoostFit(self.fitID)
self.fitBooster = not self.fitBooster self.fitBooster = not self.fitBooster
self.boosterBtn.Show(self.fitBooster) self.boosterBtn.Show(self.fitBooster)
self.fitMenu.Check(self.toggleItem.GetId(), self.fitBooster) self.Refresh()
wx.PostEvent(self.mainFrame, BoosterListUpdated()) wx.PostEvent(self.mainFrame, BoosterListUpdated())
event.Skip() event.Skip()
@@ -1545,9 +1528,23 @@ class FitItem(SFItem.SFBrowserItem):
''' Handles context menu for fit. Dragging is handled by MouseLeftUp() ''' ''' Handles context menu for fit. Dragging is handled by MouseLeftUp() '''
pos = wx.GetMousePosition() pos = wx.GetMousePosition()
pos = self.ScreenToClient(pos) pos = self.ScreenToClient(pos)
# Even though we may not select a booster, automatically set this so that the fleet pane knows which fit we're applying # Even though we may not select a booster, automatically set this so that the fleet pane knows which fit we're applying
self.mainFrame.additionsPane.gangPage.draggedFitID = self.fitID self.mainFrame.additionsPane.gangPage.draggedFitID = self.fitID
self.PopupMenu(self.fitMenu, pos)
menu = wx.Menu()
toggleItem = menu.Append(wx.ID_ANY, "Booster Fit", kind=wx.ITEM_CHECK)
menu.Check(toggleItem.GetId(), self.fitBooster)
self.Bind(wx.EVT_MENU, self.OnToggleBooster, toggleItem)
if self.mainFrame.getActiveFit():
# If there is an active fit, get menu for setting individual boosters
menu.AppendSeparator()
boosterMenu = self.mainFrame.additionsPane.gangPage.FitDNDPopupMenu
menu.AppendSubMenu(boosterMenu, 'Set Booster')
self.PopupMenu(menu, pos)
event.Skip() event.Skip()

BIN
icons/refresh_small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

258
scripts/icons_update.py Normal file
View File

@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""
This script updates only market/item icons.
"""
import argparse
import os
import re
import sqlite3
from PIL import Image
parser = argparse.ArgumentParser(description='This script updates module icons for pyfa')
parser.add_argument('-i', '--icons', required=True, type=str, help='path to unpacked Icons folder from CCP\'s image export')
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__))
db_path = os.path.abspath(os.path.join(script_dir, '..', 'staticdata', 'eve.db'))
icons_dir = os.path.abspath(os.path.join(script_dir, '..', 'staticdata', 'icons'))
export_dir = os.path.abspath(os.path.expanduser(os.path.join(args.icons, 'items')))
db = sqlite3.connect(db_path)
cursor = db.cursor()
ICON_SIZE = (16, 16)
ITEM_CATEGORIES = (
6, # Ship
7, # Module
8, # Charge
16, # Skill
18, # Drone
20, # Implant
32 # Subsystem
)
MARKET_ROOTS = {
9, # Modules
1111, # Rigs
157, # Drones
11, # Ammo
1112, # Subsystems
24, # Implants & Boosters
404 # Deployables
}
# Add children to market group list
# {parent: {children}}
mkt_tree = {}
for row in cursor.execute('select marketGroupID, parentGroupID from invmarketgroups'):
parent = row[1]
# We have all the root groups in the set we need anyway
if not parent:
continue
child = row[0]
children = mkt_tree.setdefault(parent, set())
children.add(child)
# Traverse the tree we just composed to add all children for all needed roots
def get_children(parent):
children = set()
for child in mkt_tree.get(parent, ()):
children.add(child)
children.update(get_children(child))
return children
market_groups = set()
for root in MARKET_ROOTS:
market_groups.add(root)
market_groups.update(get_children(root))
query_items = 'select distinct i.iconFile from icons as i inner join invtypes as it on it.iconID = i.iconID inner join invgroups as ig on it.groupID = ig.groupID where ig.categoryID in ({})'.format(', '.join(str(i) for i in ITEM_CATEGORIES))
query_groups = 'select distinct i.iconFile from icons as i inner join invgroups as ig on ig.iconID = i.iconID where ig.categoryID in ({})'.format(', '.join(str(i) for i in ITEM_CATEGORIES))
query_cats = 'select distinct i.iconFile from icons as i inner join invcategories as ic on ic.iconID = i.iconID where ic.categoryID in ({})'.format(', '.join(str(i) for i in ITEM_CATEGORIES))
query_market = 'select distinct i.iconFile from icons as i inner join invmarketgroups as img on img.iconID = i.iconID where img.marketGroupID in ({})'.format(', '.join(str(i) for i in market_groups))
query_attrib = 'select distinct i.iconFile from icons as i inner join dgmattribs as da on da.iconID = i.iconID'
needed = set()
existing = set()
export = {}
def strip_path(fname):
"""
Here we extract 'core' of icon name. Path and
extension are sometimes specified in database
but we don't need them.
"""
# Path before the icon file name
fname = fname.split('/')[-1]
# Extension
fname = fname.rsplit('.', 1)[0]
return fname
def unzero(fname):
"""
Get rid of leading zeros in triplet. They are often specified in DB
but almost never in actual files.
"""
m = re.match(r'^(?P<prefix>[^_\.]+)_((?P<size>\d+)_)?(?P<suffix>[^_\.]+)(?P<tail>\..*)?$', fname)
if m:
prefix = m.group('prefix')
size = m.group('size')
suffix = m.group('suffix')
tail = m.group('tail')
try:
prefix = int(prefix)
except (TypeError, ValueError):
pass
try:
size = int(size)
except (TypeError, ValueError):
pass
try:
suffix = int(suffix)
except (TypeError, ValueError):
pass
if size is None:
fname = '{}_{}{}'.format(prefix, suffix, tail)
else:
fname = '{}_{}_{}{}'.format(prefix, size, suffix, tail)
return fname
else:
return fname
for query in (query_items, query_groups, query_cats, query_market, query_attrib):
for row in cursor.execute(query):
fname = row[0]
if not fname:
continue
fname = strip_path(fname)
needed.add(fname)
for fname in os.listdir(icons_dir):
if not os.path.isfile(os.path.join(icons_dir, fname)):
continue
if not fname.startswith('icon') or not fname.endswith('.png'):
continue
fname = strip_path(fname)
# Get rid of "icon" prefix as well
fname = re.sub('^icon', '', fname)
existing.add(fname)
for fname in os.listdir(export_dir):
if not os.path.isfile(os.path.join(export_dir, fname)):
continue
stripped = strip_path(fname)
stripped = unzero(stripped)
# Icons in export often specify size in their name, but references often use
# convention without size specification
sizeless = re.sub('^(?P<prefix>[^_]+)_(?P<size>\d+)_(?P<suffix>[^_]+)$', r'\1_\3', stripped)
# Often items referred to with 01_01 format,
fnames = export.setdefault(stripped, set())
fnames.add(fname)
fnames = export.setdefault(sizeless, set())
fnames.add(fname)
def crop_image(img):
w, h = img.size
if h == w:
return img
normal = min(h, w)
diff_w = w - normal
diff_h = h - normal
crop_top = diff_h // 2
crop_bot = diff_h // 2 + diff_h % 2
crop_left = diff_w // 2
crop_right = diff_w // 2 + diff_w % 2
box = (crop_left, crop_top, w - crop_right, h - crop_bot)
return img.crop(box)
def get_icon_file(request):
"""
Get the iconFile field value and find proper
icon for it. Return as PIL image object down-
scaled for use in pyfa.
"""
rq = strip_path(request)
rq = unzero(rq)
try:
fnames = export[rq]
except KeyError:
return None
# {(h, w): source full path}
sizes = {}
for fname in fnames:
fullpath = os.path.join(export_dir, fname)
img = Image.open(fullpath)
sizes[img.size] = fullpath
# Try to return image which is already in necessary format
try:
fullpath = sizes[ICON_SIZE]
# Otherwise, convert biggest image
except KeyError:
fullpath = sizes[max(sizes)]
img = Image.open(fullpath)
img = crop_image(img)
img.thumbnail(ICON_SIZE, Image.ANTIALIAS)
else:
img = Image.open(fullpath)
return img
toremove = existing.difference(needed)
toupdate = existing.intersection(needed)
toadd = needed.difference(existing)
if toremove:
print('Some icons are not used and will be removed:')
for fname in sorted(toremove):
fullname = 'icon{}.png'.format(fname)
print(' {}'.format(fullname))
fullpath = os.path.join(icons_dir, fullname)
os.remove(fullpath)
if toupdate:
print('Updating {} icons...'.format(len(toupdate)))
missing = set()
for fname in sorted(toupdate):
icon = get_icon_file(fname)
if icon is None:
missing.add(fname)
continue
fullname = 'icon{}.png'.format(fname)
fullpath = os.path.join(icons_dir, fullname)
icon.save(fullpath, 'PNG')
if missing:
print(' {} icons are missing in export:'.format(len(missing)))
for fname in sorted(missing):
print(' {}'.format(fname))
if toadd:
print('Adding {} icons...'.format(len(toadd)))
missing = set()
for fname in sorted(toadd):
icon = get_icon_file(fname)
if icon is None:
missing.add(fname)
continue
fullname = 'icon{}.png'.format(fname)
fullpath = os.path.join(icons_dir, fullname)
icon.save(fullpath, 'PNG')
if missing:
print(' {} icons are missing in export:'.format(len(missing)))
for fname in sorted(missing):
print(' {}'.format(fname))

View File

@@ -1,202 +1,212 @@
#!/usr/bin/env python #!/usr/bin/env python
#====================================================================== #======================================================================
# Copyright (C) 2012 Diego Duclos # Copyright (C) 2012 Diego Duclos
# #
# This file is part of eos. # This file is part of eos.
# #
# eos is free software: you can redistribute it and/or modify # eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as # it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of # published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version. # the License, or (at your option) any later version.
# #
# eos is distributed in the hope that it will be useful, # eos is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details. # GNU Lesser General Public License for more details.
# #
# You should have received a copy of the GNU Lesser General Public # You should have received a copy of the GNU Lesser General Public
# License along with eos. If not, see <http://www.gnu.org/licenses/>. # License along with eos. If not, see <http://www.gnu.org/licenses/>.
#====================================================================== #======================================================================
import os import os
import sys import sys
# Add eos root path to sys.path so we can import ourselves # Add eos root path to sys.path so we can import ourselves
path = os.path.dirname(unicode(__file__, sys.getfilesystemencoding())) path = os.path.dirname(unicode(__file__, sys.getfilesystemencoding()))
sys.path.append(os.path.realpath(os.path.join(path, ".."))) sys.path.append(os.path.realpath(os.path.join(path, "..")))
import json import json
import argparse import argparse
def main(db, json_path): def main(db, json_path):
jsonPath = os.path.expanduser(json_path) jsonPath = os.path.expanduser(json_path)
# Import eos.config first and change it # Import eos.config first and change it
import eos.config import eos.config
eos.config.gamedata_connectionstring = db eos.config.gamedata_connectionstring = db
eos.config.debug = False eos.config.debug = False
# Now thats done, we can import the eos modules using the config # Now thats done, we can import the eos modules using the config
import eos.db import eos.db
import eos.gamedata import eos.gamedata
# Create the database tables # Create the database tables
eos.db.gamedata_meta.create_all() eos.db.gamedata_meta.create_all()
# Config dict # Config dict
tables = { tables = {
"dgmattribs": eos.gamedata.AttributeInfo, "dgmattribs": eos.gamedata.AttributeInfo,
"dgmeffects": eos.gamedata.EffectInfo, "dgmeffects": eos.gamedata.EffectInfo,
"dgmtypeattribs": eos.gamedata.Attribute, "dgmtypeattribs": eos.gamedata.Attribute,
"dgmtypeeffects": eos.gamedata.Effect, "dgmtypeeffects": eos.gamedata.Effect,
"dgmunits": eos.gamedata.Unit, "dgmunits": eos.gamedata.Unit,
"icons": eos.gamedata.Icon, "icons": eos.gamedata.Icon,
"invcategories": eos.gamedata.Category, "evecategories": eos.gamedata.Category,
"invgroups": eos.gamedata.Group, "evegroups": eos.gamedata.Group,
"invmetagroups": eos.gamedata.MetaGroup, "invmetagroups": eos.gamedata.MetaGroup,
"invmetatypes": eos.gamedata.MetaType, "invmetatypes": eos.gamedata.MetaType,
"invtypes": eos.gamedata.Item, "evetypes": eos.gamedata.Item,
"phbtraits": eos.gamedata.Traits, "phbtraits": eos.gamedata.Traits,
"phbmetadata": eos.gamedata.MetaData, "phbmetadata": eos.gamedata.MetaData,
"mapbulk_marketGroups": eos.gamedata.MarketGroup "mapbulk_marketGroups": eos.gamedata.MarketGroup
} }
fieldMapping = { fieldMapping = {
"dgmattribs": { "dgmattribs": {
"displayName_en-us": "displayName" "displayName_en-us": "displayName"
}, },
"dgmeffects": { "dgmeffects": {
"displayName_en-us": "displayName", "displayName_en-us": "displayName",
"description_en-us": "description" "description_en-us": "description"
}, },
"dgmunits": { "dgmunits": {
"displayName_en-us": "displayName" "displayName_en-us": "displayName"
}, },
#icons??? #icons???
"invcategories": { "evecategories": {
"categoryName_en-us": "categoryName" "categoryName_en-us": "categoryName"
}, },
"invgroups": { "evegroups": {
"groupName_en-us": "groupName" "groupName_en-us": "groupName"
}, },
"invmetagroups": { "invmetagroups": {
"metaGroupName_en-us": "metaGroupName" "metaGroupName_en-us": "metaGroupName"
}, },
"invtypes": { "evetypes": {
"typeName_en-us": "typeName", "typeName_en-us": "typeName",
"description_en-us": "description" "description_en-us": "description"
}, },
#phbtraits??? #phbtraits???
"mapbulk_marketGroups": { "mapbulk_marketGroups": {
"marketGroupName_en-us": "marketGroupName", "marketGroupName_en-us": "marketGroupName",
"description_en-us": "description" "description_en-us": "description"
} }
} }
def convertIcons(data): rowsInValues = (
new = [] "evetypes",
for k, v in data.items(): "evegroups",
v["iconID"] = k "evecategories"
new.append(v) )
return new
def convertIcons(data):
def convertTraits(data): new = []
for k, v in data.items():
def convertSection(sectionData): v["iconID"] = k
sectionLines = [] new.append(v)
headerText = u"<b>{}</b>".format(sectionData["header"]) return new
sectionLines.append(headerText)
for bonusData in sectionData["bonuses"]: def convertTraits(data):
prefix = u"{} ".format(bonusData["number"]) if "number" in bonusData else ""
bonusText = u"{}{}".format(prefix, bonusData["text"].replace(u"\u00B7", u"\u2022 ")) def convertSection(sectionData):
sectionLines.append(bonusText) sectionLines = []
sectionLine = u"<br />\n".join(sectionLines) headerText = u"<b>{}</b>".format(sectionData["header"])
return sectionLine sectionLines.append(headerText)
for bonusData in sectionData["bonuses"]:
newData = [] prefix = u"{} ".format(bonusData["number"]) if "number" in bonusData else ""
for row in data: bonusText = u"{}{}".format(prefix, bonusData["text"].replace(u"\u00B7", u"\u2022 "))
typeLines = [] sectionLines.append(bonusText)
typeId = row["typeID"] sectionLine = u"<br />\n".join(sectionLines)
traitData = row["traits_en-us"] return sectionLine
for skillData in sorted(traitData.get("skills", ()), key=lambda i: i["header"]):
typeLines.append(convertSection(skillData)) newData = []
if "role" in traitData: for row in data:
typeLines.append(convertSection(traitData["role"])) typeLines = []
if "misc" in traitData: typeId = row["typeID"]
typeLines.append(convertSection(traitData["misc"])) traitData = row["traits_en-us"]
traitLine = u"<br />\n<br />\n".join(typeLines) for skillData in sorted(traitData.get("skills", ()), key=lambda i: i["header"]):
newRow = {"typeID": typeId, "traitText": traitLine} typeLines.append(convertSection(skillData))
newData.append(newRow) if "role" in traitData:
return newData typeLines.append(convertSection(traitData["role"]))
if "misc" in traitData:
def convertTypes(typesData): typeLines.append(convertSection(traitData["misc"]))
""" traitLine = u"<br />\n<br />\n".join(typeLines)
Add factionID column to invtypes table. newRow = {"typeID": typeId, "traitText": traitLine}
""" newData.append(newRow)
factionMap = {} return newData
with open(os.path.join(jsonPath, "fsdTypeOverrides.json")) as f:
overridesData = json.load(f) def convertTypes(typesData):
for typeID, typeData in overridesData.items(): """
factionID = typeData.get("factionID") Add factionID column to evetypes table.
if factionID is not None: """
factionMap[int(typeID)] = factionID factionMap = {}
for row in typesData: with open(os.path.join(jsonPath, "fsdTypeOverrides.json")) as f:
row['factionID'] = factionMap.get(int(row['typeID'])) overridesData = json.load(f)
return typesData for typeID, typeData in overridesData.items():
factionID = typeData.get("factionID")
data = {} if factionID is not None:
factionMap[int(typeID)] = factionID
# Dump all data to memory so we can easely cross check ignored rows for row in typesData:
for jsonName, cls in tables.iteritems(): row['factionID'] = factionMap.get(int(row['typeID']))
with open(os.path.join(jsonPath, "{}.json".format(jsonName))) as f: return typesData
tableData = json.load(f)
if jsonName == "icons": data = {}
tableData = convertIcons(tableData)
if jsonName == "phbtraits": # Dump all data to memory so we can easely cross check ignored rows
tableData = convertTraits(tableData) for jsonName, cls in tables.iteritems():
if jsonName == "invtypes": with open(os.path.join(jsonPath, "{}.json".format(jsonName))) as f:
tableData = convertTypes(tableData) tableData = json.load(f)
data[jsonName] = tableData if jsonName in rowsInValues:
tableData = list(tableData.values())
# Set with typeIDs which we will have in our database if jsonName == "icons":
invTypes = set() tableData = convertIcons(tableData)
for row in data["invtypes"]: if jsonName == "phbtraits":
# 1306 - group Ship Modifiers, for items like tactical t3 ship modes tableData = convertTraits(tableData)
if (row["published"] or row['groupID'] == 1306): if jsonName == "evetypes":
invTypes.add(row["typeID"]) tableData = convertTypes(tableData)
data[jsonName] = tableData
# ignore checker
def isIgnored(file, row): # Set with typeIDs which we will have in our database
if file in ("invtypes", "dgmtypeeffects", "dgmtypeattribs", "invmetatypes") and row['typeID'] not in invTypes: # Sometimes CCP unpublishes some items we want to have published, we
return True # can do it here - just add them to initial set
return False eveTypes = set()
for row in data["evetypes"]:
# Loop through each json file and write it away, checking ignored rows # 1306 - group Ship Modifiers, for items like tactical t3 ship modes
for jsonName, table in data.iteritems(): if (row["published"] or row['groupID'] == 1306):
fieldMap = fieldMapping.get(jsonName, {}) eveTypes.add(row["typeID"])
print "processing {}".format(jsonName)
for row in table: # ignore checker
# We don't care about some kind of rows, filter it out if so def isIgnored(file, row):
if not isIgnored(jsonName, row): if file in ("evetypes", "dgmtypeeffects", "dgmtypeattribs", "invmetatypes") and row['typeID'] not in eveTypes:
instance = tables[jsonName]() return True
# fix for issue 80 return False
if jsonName is "icons" and "res:/UI/Texture/Icons/" in str(row["iconFile"]):
row["iconFile"] = row["iconFile"].replace("res:/UI/Texture/Icons/","").replace(".png", "") # Loop through each json file and write it away, checking ignored rows
for k, v in row.iteritems(): for jsonName, table in data.iteritems():
setattr(instance, fieldMap.get(k, k), v) fieldMap = fieldMapping.get(jsonName, {})
print "processing {}".format(jsonName)
eos.db.gamedata_session.add(instance) for row in table:
# We don't care about some kind of rows, filter it out if so
eos.db.gamedata_session.commit() if not isIgnored(jsonName, row):
instance = tables[jsonName]()
print("done") # fix for issue 80
if jsonName is "icons" and "res:/UI/Texture/Icons/" in str(row["iconFile"]):
if __name__ == "__main__": row["iconFile"] = row["iconFile"].replace("res:/UI/Texture/Icons/","").replace(".png", "")
parser = argparse.ArgumentParser(description="This scripts dumps effects from an sqlite cache dump to mongo") for k, v in row.iteritems():
parser.add_argument("-d", "--db", required=True, type=str, help="The sqlalchemy connectionstring, example: sqlite:///c:/tq.db") setattr(instance, fieldMap.get(k, k), v)
parser.add_argument("-j", "--json", required=True, type=str, help="The path to the json dump")
args = parser.parse_args() eos.db.gamedata_session.add(instance)
main(args.db, args.json) eos.db.gamedata_session.commit()
print("done")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="This scripts dumps effects from an sqlite cache dump to mongo")
parser.add_argument("-d", "--db", required=True, type=str, help="The sqlalchemy connectionstring, example: sqlite:///c:/tq.db")
parser.add_argument("-j", "--json", required=True, type=str, help="The path to the json dump")
args = parser.parse_args()
main(args.db, args.json)

120
scripts/renders_update.py Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
This script updates ship renders and removes unused ones.
"""
import argparse
import os
import re
import sqlite3
from PIL import Image
parser = argparse.ArgumentParser(description='This script updates ship renders for pyfa')
parser.add_argument('-r', '--renders', required=True, type=str, help='path to unpacked Renders folder from CCP\'s image export')
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__))
db_path = os.path.abspath(os.path.join(script_dir, '..', 'staticdata', 'eve.db'))
icons_dir = os.path.abspath(os.path.join(script_dir, '..', 'staticdata', 'icons', 'ships'))
export_dir = os.path.abspath(os.path.expanduser(args.renders))
db = sqlite3.connect(db_path)
cursor = db.cursor()
RENDER_SIZE = (32, 32)
query_ships = 'select it.typeID from invtypes as it inner join invgroups as ig on it.groupID = ig.groupID where ig.categoryID = 6'
needed = set()
existing = set()
export = set()
for row in cursor.execute(query_ships):
needed.add(row[0])
for container, filedir in (
(existing, icons_dir),
(export, export_dir)
):
for fname in os.listdir(filedir):
if not os.path.isfile(os.path.join(filedir, fname)):
continue
m = re.match(r'^(?P<typeid>\d+)\.png', fname)
if not m:
continue
container.add(int(m.group('typeid')))
toremove = existing.difference(needed)
toupdate = existing.intersection(needed)
toadd = needed.difference(existing)
def crop_image(img):
w, h = img.size
if h == w:
return img
normal = min(h, w)
diff_w = w - normal
diff_h = h - normal
crop_top = diff_h // 2
crop_bot = diff_h // 2 + diff_h % 2
crop_left = diff_w // 2
crop_right = diff_w // 2 + diff_w % 2
box = (crop_left, crop_top, w - crop_right, h - crop_bot)
return img.crop(box)
def get_render(type_id):
fname = '{}.png'.format(type_id)
fullpath = os.path.join(export_dir, fname)
img = Image.open(fullpath)
if img.size != RENDER_SIZE:
img = crop_image(img)
img.thumbnail(RENDER_SIZE, Image.ANTIALIAS)
return img
if toremove:
print('Some renders are not used and will be removed:')
for type_id in sorted(toremove):
fullname = '{}.png'.format(type_id)
print(' {}'.format(fullname))
fullpath = os.path.join(icons_dir, fullname)
os.remove(fullpath)
if toupdate:
print('Updating {} renders...'.format(len(toupdate)))
missing = toupdate.difference(export)
toupdate.intersection_update(export)
for type_id in sorted(toupdate):
render = get_render(type_id)
fname = '{}.png'.format(type_id)
fullpath = os.path.join(icons_dir, fname)
render.save(fullpath, 'PNG')
if missing:
print(' {} renders are missing in export:'.format(len(missing)))
for type_id in sorted(missing):
print(' {}.png'.format(type_id))
if toadd:
print('Adding {} renders...'.format(len(toadd)))
missing = toadd.difference(export)
toadd.intersection_update(export)
for type_id in sorted(toadd):
render = get_render(type_id)
fname = '{}.png'.format(type_id)
fullpath = os.path.join(icons_dir, fname)
render.save(fullpath, 'PNG')
if missing:
print(' {} renders are missing in export:'.format(len(missing)))
for type_id in sorted(missing):
print(' {}.png'.format(type_id))

View File

@@ -0,0 +1,146 @@
"""
Conversion pack for Carnyx Module Tiericide
"""
CONVERSIONS = {
# Renamed items
"1MN Microwarpdrive I": "5MN Microwarpdrive I",
"1MN Microwarpdrive II": "5MN Microwarpdrive II",
"Prototype 100MN Microwarpdrive I": "500MN Cold-Gas Enduring Microwarpdrive",
"Experimental 100MN Afterburner I": "100MN Monopropellant Enduring Afterburner",
"Upgraded 1MN Microwarpdrive I": "5MN Cold-Gas Enduring Microwarpdrive",
"Limited 1MN Microwarpdrive I": "5MN Y-T8 Compact Microwarpdrive",
"Experimental 10MN Microwarpdrive I": "50MN Cold-Gas Enduring Microwarpdrive",
"Limited 1MN Afterburner I": "1MN Y-S8 Compact Afterburner",
"Experimental 1MN Afterburner I": "1MN Monopropellant Enduring Afterburner",
"Experimental 10MN Afterburner I": "10MN Monopropellant Enduring Afterburner",
"Large Azeotropic Ward Salubrity I": "Large Azeotropic Restrained Shield Extender",
"Small Azeotropic Ward Salubrity I": "Small Azeotropic Restrained Shield Extender",
"Medium Azeotropic Ward Salubrity I": "Medium Azeotropic Restrained Shield Extender",
"Medium F-S9 Regolith Shield Induction": "Medium F-S9 Regolith Compact Shield Extender",
"Small F-S9 Regolith Shield Induction": "Small F-S9 Regolith Compact Shield Extender",
"Large F-S9 Regolith Shield Induction": "Large F-S9 Regolith Compact Shield Extender",
"1600mm Reinforced Steel Plates I": "1600mm Steel Plates I",
"100mm Reinforced Steel Plates I": "100mm Steel Plates I",
"200mm Reinforced Steel Plates I": "200mm Steel Plates I",
"400mm Reinforced Steel Plates I": "400mm Steel Plates I",
"800mm Reinforced Steel Plates I": "800mm Steel Plates I",
"400mm Reinforced Rolled Tungsten Plates I": "400mm Rolled Tungsten Compact Plates",
"400mm Reinforced Crystalline Carbonide Plates I": "400mm Crystalline Carbonide Restrained Plates",
"800mm Reinforced Rolled Tungsten Plates I": "800mm Rolled Tungsten Compact Plates",
"800mm Reinforced Crystalline Carbonide Plates I": "800mm Crystalline Carbonide Restrained Plates",
"1600mm Reinforced Rolled Tungsten Plates I": "1600mm Rolled Tungsten Compact Plates",
"1600mm Reinforced Crystalline Carbonide Plates I": "1600mm Crystalline Carbonide Restrained Plates",
"100mm Reinforced Rolled Tungsten Plates I": "100mm Rolled Tungsten Compact Plates",
"100mm Reinforced Crystalline Carbonide Plates I": "100mm Crystalline Carbonide Restrained Plates",
"200mm Reinforced Rolled Tungsten Plates I": "200mm Rolled Tungsten Compact Plates",
"200mm Reinforced Crystalline Carbonide Plates I": "200mm Crystalline Carbonide Restrained Plates",
"10MN Microwarpdrive I": "50MN Microwarpdrive I",
"100MN Microwarpdrive I": "500MN Microwarpdrive I",
"10MN Microwarpdrive II": "50MN Microwarpdrive II",
"100MN Microwarpdrive II": "500MN Microwarpdrive II",
"Domination 100MN Microwarpdrive": "Domination 500MN Microwarpdrive",
"Shadow Serpentis 100MN Microwarpdrive": "Shadow Serpentis 500MN Microwarpdrive",
"Domination 10MN Microwarpdrive": "Domination 50MN Microwarpdrive",
"Shadow Serpentis 10MN Microwarpdrive": "Shadow Serpentis 50MN Microwarpdrive",
"Domination 1MN Microwarpdrive": "Domination 5MN Microwarpdrive",
"Shadow Serpentis 1MN Microwarpdrive": "Shadow Serpentis 5MN Microwarpdrive",
"Mizuro's Modified 100MN Microwarpdrive": "Mizuro's Modified 500MN Microwarpdrive",
"Hakim's Modified 100MN Microwarpdrive": "Hakim's Modified 500MN Microwarpdrive",
"Gotan's Modified 100MN Microwarpdrive": "Gotan's Modified 500MN Microwarpdrive",
"Tobias' Modified 100MN Microwarpdrive": "Tobias' Modified 500MN Microwarpdrive",
"Brynn's Modified 100MN Microwarpdrive": "Brynn's Modified 500MN Microwarpdrive",
"Tuvan's Modified 100MN Microwarpdrive": "Tuvan's Modified 500MN Microwarpdrive",
"Setele's Modified 100MN Microwarpdrive": "Setele's Modified 500MN Microwarpdrive",
"Cormack's Modified 100MN Microwarpdrive": "Cormack's Modified 500MN Microwarpdrive",
"Republic Fleet 1MN Microwarpdrive": "Republic Fleet 5MN Microwarpdrive",
"Republic Fleet 10MN Microwarpdrive": "Republic Fleet 50MN Microwarpdrive",
"Republic Fleet 100MN Microwarpdrive": "Republic Fleet 500MN Microwarpdrive",
"Federation Navy 1MN Microwarpdrive": "Federation Navy 5MN Microwarpdrive",
"Federation Navy 10MN Microwarpdrive": "Federation Navy 50MN Microwarpdrive",
"Federation Navy 100MN Microwarpdrive": "Federation Navy 500MN Microwarpdrive",
"Coreli C-Type 1MN Microwarpdrive": "Coreli C-Type 5MN Microwarpdrive",
"Corelum C-Type 10MN Microwarpdrive": "Corelum C-Type 50MN Microwarpdrive",
"Core C-Type 100MN Microwarpdrive": "Core C-Type 500MN Microwarpdrive",
"Coreli B-Type 1MN Microwarpdrive": "Coreli B-Type 5MN Microwarpdrive",
"Corelum B-Type 10MN Microwarpdrive": "Corelum B-Type 50MN Microwarpdrive",
"Core B-Type 100MN Microwarpdrive": "Core B-Type 500MN Microwarpdrive",
"Coreli A-Type 1MN Microwarpdrive": "Coreli A-Type 5MN Microwarpdrive",
"Corelum A-Type 10MN Microwarpdrive": "Corelum A-Type 50MN Microwarpdrive",
"Core A-Type 100MN Microwarpdrive": "Core A-Type 500MN Microwarpdrive",
"Core X-Type 100MN Microwarpdrive": "Core X-Type 500MN Microwarpdrive",
"Gistii C-Type 1MN Microwarpdrive": "Gistii C-Type 5MN Microwarpdrive",
"Gistum C-Type 10MN Microwarpdrive": "Gistum C-Type 50MN Microwarpdrive",
"Gist C-Type 100MN Microwarpdrive": "Gist C-Type 500MN Microwarpdrive",
"Gistii B-Type 1MN Microwarpdrive": "Gistii B-Type 5MN Microwarpdrive",
"Gistum B-Type 10MN Microwarpdrive": "Gistum B-Type 50MN Microwarpdrive",
"Gist B-Type 100MN Microwarpdrive": "Gist B-Type 500MN Microwarpdrive",
"Gistii A-Type 1MN Microwarpdrive": "Gistii A-Type 5MN Microwarpdrive",
"Gistum A-Type 10MN Microwarpdrive": "Gistum A-Type 50MN Microwarpdrive",
"Gist A-Type 100MN Microwarpdrive": "Gist A-Type 500MN Microwarpdrive",
"Gist X-Type 100MN Microwarpdrive": "Gist X-Type 500MN Microwarpdrive",
"100mm Reinforced Steel Plates II": "100mm Steel Plates II",
"200mm Reinforced Steel Plates II": "200mm Steel Plates II",
"400mm Reinforced Steel Plates II": "400mm Steel Plates II",
"800mm Reinforced Steel Plates II": "800mm Steel Plates II",
"1600mm Reinforced Steel Plates II": "1600mm Steel Plates II",
"Micro 'Trapper' Shield Extender": "Small 'Trapper' Shield Extender",
"1MN Analog Booster Rockets": "1MN Analog Booster Afterburner",
"10MN Analog Booster Rockets": "10MN Analog Booster Afterburner",
"100MN Analog Booster Rockets": "100MN Analog Booster Afterburner",
"1MN Digital Booster Rockets": "5MN Digital Booster Microwarpdrive",
"10MN Digital Booster Rockets": "50MN Digital Booster Microwarpdrive",
"100MN Digital Booster Rockets": "500MN Digital Booster Microwarpdrive",
"Civilian Afterburner": "1MN Civilian Afterburner",
"'Abatis' 100mm Reinforced Steel Plates I": "'Abatis' 100mm Steel Plates",
"'Bailey' 1600mm Reinforced Steel Plates I": "'Bailey' 1600mm Steel Plates",
"'Chainmail' 200mm Reinforced Steel Plates I": "'Chainmail' 200mm Steel Plates",
"'Bastion' 400mm Reinforced Steel Plates I": "'Bastion' 400mm Steel Plates",
"'Citadella' 50mm Reinforced Steel Plates I": "'Citadella' 100mm Steel Plates",
"'Barbican' 800mm Reinforced Steel Plates I": "'Barbican' 800mm Steel Plates",
"Syndicate 100mm Reinforced Steel Plates": "Syndicate 100mm Steel Plates",
"Syndicate 1600mm Reinforced Steel Plates": "Syndicate 1600mm Steel Plates",
"Syndicate 200mm Reinforced Steel Plates": "Syndicate 200mm Steel Plates",
"Syndicate 400mm Reinforced Steel Plates": "Syndicate 400mm Steel Plates",
"Syndicate 800mm Reinforced Steel Plates": "Syndicate 800mm Steel Plates",
"Imperial Navy 100mm Reinforced Steel Plates": "Imperial Navy 100mm Steel Plates",
"Federation Navy 100mm Reinforced Steel Plates": "Federation Navy 100mm Steel Plates",
"Imperial Navy 1600mm Reinforced Steel Plates": "Imperial Navy 1600mm Steel Plates",
"Federation Navy 1600mm Reinforced Steel Plates": "Federation Navy 1600mm Steel Plates",
"Imperial Navy 200mm Reinforced Steel Plates": "Imperial Navy 200mm Steel Plates",
"Federation Navy 200mm Reinforced Steel Plates": "Federation Navy 200mm Steel Plates",
"Imperial Navy 400mm Reinforced Steel Plates": "Imperial Navy 400mm Steel Plates",
"Federation Navy 400mm Reinforced Steel Plates": "Federation Navy 400mm Steel Plates",
"Imperial Navy 800mm Reinforced Steel Plates": "Imperial Navy 800mm Steel Plates",
"Federation Navy 800mm Reinforced Steel Plates": "Federation Navy 800mm Steel Plates",
"Polarized Small Pulse Laser": "Polarized Small Focused Pulse Laser",
# Converted items
"Large Subordinate Screen Stabilizer I": "Large F-S9 Regolith Compact Shield Extender",
"Large Supplemental Barrier Emitter I": "Large Azeotropic Restrained Shield Extender",
"Medium Subordinate Screen Stabilizer I": "Medium F-S9 Regolith Compact Shield Extender",
"Medium Supplemental Barrier Emitter I": "Medium Azeotropic Restrained Shield Extender",
"Micro Azeotropic Ward Salubrity I": "Small 'Trapper' Shield Extender",
"Micro F-S9 Regolith Shield Induction": "Small 'Trapper' Shield Extender",
"Micro Shield Extender I": "Small 'Trapper' Shield Extender",
"Micro Shield Extender II": "Small 'Trapper' Shield Extender",
"Micro Subordinate Screen Stabilizer I": "Small 'Trapper' Shield Extender",
"Micro Supplemental Barrier Emitter I": "Small 'Trapper' Shield Extender",
"Small Subordinate Screen Stabilizer I": "Small F-S9 Regolith Compact Shield Extender",
"Small Supplemental Barrier Emitter I": "Small Azeotropic Restrained Shield Extender",
"100mm Reinforced Nanofiber Plates I": "100mm Crystalline Carbonide Restrained Plates",
"100mm Reinforced Titanium Plates I": "100mm Rolled Tungsten Compact Plates",
"1600mm Reinforced Nanofiber Plates I": "1600mm Crystalline Carbonide Restrained Plates",
"1600mm Reinforced Titanium Plates I": "1600mm Rolled Tungsten Compact Plates",
"200mm Reinforced Nanofiber Plates I": "200mm Crystalline Carbonide Restrained Plates",
"200mm Reinforced Titanium Plates I": "200mm Rolled Tungsten Compact Plates",
"400mm Reinforced Nanofiber Plates I": "400mm Crystalline Carbonide Restrained Plates",
"400mm Reinforced Titanium Plates I": "400mm Rolled Tungsten Compact Plates",
"50mm Reinforced Crystalline Carbonide Plates I": "'Citadella' 100mm Steel Plates",
"50mm Reinforced Nanofiber Plates I": "'Citadella' 100mm Steel Plates",
"50mm Reinforced Rolled Tungsten Plates I": "'Citadella' 100mm Steel Plates",
"50mm Reinforced Steel Plates I": "'Citadella' 100mm Steel Plates",
"50mm Reinforced Steel Plates II": "'Citadella' 100mm Steel Plates",
"50mm Reinforced Titanium Plates I": "'Citadella' 100mm Steel Plates",
"800mm Reinforced Nanofiber Plates I": "800mm Crystalline Carbonide Restrained Plates",
"800mm Reinforced Titanium Plates I": "800mm Rolled Tungsten Compact Plates"
}

View File

@@ -36,7 +36,7 @@ from service.fleet import Fleet
from service.settings import SettingsProvider from service.settings import SettingsProvider
from service.port import Port from service.port import Port
logger = logging.getLogger("pyfa.service.fit") logger = logging.getLogger(__name__)
class FitBackupThread(threading.Thread): class FitBackupThread(threading.Thread):
def __init__(self, path, callback): def __init__(self, path, callback):
@@ -97,7 +97,8 @@ class Fit(object):
"rackSlots": True, "rackSlots": True,
"rackLabels": True, "rackLabels": True,
"compactSkills": True, "compactSkills": True,
"showTooltip": True} "showTooltip": True,
"showMarketShortcuts": False}
self.serviceFittingOptions = SettingsProvider.getInstance().getSettings( self.serviceFittingOptions = SettingsProvider.getInstance().getSettings(
"pyfaServiceFittingOptions", serviceFittingDefaultOptions) "pyfaServiceFittingOptions", serviceFittingDefaultOptions)
@@ -149,8 +150,8 @@ class Fit(object):
return fit.modules[pos] return fit.modules[pos]
def newFit(self, shipID, name=None): def newFit(self, shipID, name=None):
fit = eos.types.Fit() ship = eos.types.Ship(eos.db.getItem(shipID))
fit.ship = eos.types.Ship(eos.db.getItem(shipID)) fit = eos.types.Fit(ship)
fit.name = name if name is not None else "New %s" % fit.ship.item.name fit.name = name if name is not None else "New %s" % fit.ship.item.name
fit.damagePattern = self.pattern fit.damagePattern = self.pattern
fit.targetResists = self.targetResists fit.targetResists = self.targetResists
@@ -174,10 +175,14 @@ class Fit(object):
fit = eos.db.getFit(fitID) fit = eos.db.getFit(fitID)
sFleet = Fleet.getInstance() sFleet = Fleet.getInstance()
sFleet.removeAssociatedFleetData(fit) sFleet.removeAssociatedFleetData(fit)
self.removeProjectedData(fitID)
eos.db.remove(fit) eos.db.remove(fit)
# refresh any fits this fit is projected onto. Otherwise, if we have
# already loaded those fits, they will not reflect the changes
for projection in fit.projectedOnto.values():
eos.db.saveddata_session.refresh(projection.victim_fit)
def copyFit(self, fitID): def copyFit(self, fitID):
fit = eos.db.getFit(fitID) fit = eos.db.getFit(fitID)
newFit = copy.deepcopy(fit) newFit = copy.deepcopy(fit)
@@ -192,14 +197,6 @@ class Fit(object):
fit.clear() fit.clear()
return fit return fit
def removeProjectedData(self, fitID):
"""Removes projection relation from ships that have fitID as projection. See GitHub issue #90"""
fit = eos.db.getFit(fitID)
fits = eos.db.getProjectedFits(fitID)
for projectee in fits:
projectee.projectedFits.remove(fit)
def toggleFactorReload(self, fitID): def toggleFactorReload(self, fitID):
if fitID is None: if fitID is None:
return None return None
@@ -235,6 +232,7 @@ class Fit(object):
return None return None
fit = eos.db.getFit(fitID) fit = eos.db.getFit(fitID)
inited = getattr(fit, "inited", None) inited = getattr(fit, "inited", None)
if inited is None or inited is False: if inited is None or inited is False:
sFleet = Fleet.getInstance() sFleet = Fleet.getInstance()
f = sFleet.getLinearFleet(fit) f = sFleet.getLinearFleet(fit)
@@ -245,6 +243,7 @@ class Fit(object):
fit.fleet = f fit.fleet = f
if not projected: if not projected:
print "Not projected, getting projected fits"
for fitP in fit.projectedFits: for fitP in fit.projectedFits:
self.getFit(fitP.ID, projected = True) self.getFit(fitP.ID, projected = True)
self.recalc(fit, withBoosters=True) self.recalc(fit, withBoosters=True)
@@ -274,7 +273,6 @@ class Fit(object):
except ValueError: except ValueError:
return False return False
fit.implants.freeSlot(implant)
fit.implants.append(implant) fit.implants.append(implant)
self.recalc(fit) self.recalc(fit)
return True return True
@@ -300,7 +298,6 @@ class Fit(object):
except ValueError: except ValueError:
return False return False
fit.boosters.freeSlot(booster)
fit.boosters.append(booster) fit.boosters.append(booster)
self.recalc(fit) self.recalc(fit)
return True return True
@@ -323,9 +320,14 @@ class Fit(object):
eager=("attributes", "group.category")) eager=("attributes", "group.category"))
if isinstance(thing, eos.types.Fit): if isinstance(thing, eos.types.Fit):
if thing.ID == fitID: if thing in fit.projectedFits:
return return
fit.projectedFits.append(thing)
fit.__projectedFits[thing.ID] = thing
# this bit is required -- see GH issue # 83
eos.db.saveddata_session.flush()
eos.db.saveddata_session.refresh(thing)
elif thing.category.name == "Drone": elif thing.category.name == "Drone":
drone = None drone = None
for d in fit.projectedDrones.find(thing): for d in fit.projectedDrones.find(thing):
@@ -364,6 +366,22 @@ class Fit(object):
thing.state = self.__getProposedState(thing, click) thing.state = self.__getProposedState(thing, click)
if not thing.canHaveState(thing.state, fit): if not thing.canHaveState(thing.state, fit):
thing.state = State.OFFLINE thing.state = State.OFFLINE
elif isinstance(thing, eos.types.Fit):
print "toggle fit"
projectionInfo = thing.getProjectionInfo(fitID)
if projectionInfo:
projectionInfo.active = not projectionInfo.active
eos.db.commit()
self.recalc(fit)
def changeAmount(self, fitID, projected_fit, amount):
"""Change amount of projected fits"""
fit = eos.db.getFit(fitID)
amount = min(20, max(1, amount)) # 1 <= a <= 20
projectionInfo = projected_fit.getProjectionInfo(fitID)
if projectionInfo:
projectionInfo.amount = amount
eos.db.commit() eos.db.commit()
self.recalc(fit) self.recalc(fit)
@@ -375,7 +393,8 @@ class Fit(object):
elif isinstance(thing, eos.types.Module): elif isinstance(thing, eos.types.Module):
fit.projectedModules.remove(thing) fit.projectedModules.remove(thing)
else: else:
fit.projectedFits.remove(thing) del fit.__projectedFits[thing.ID]
#fit.projectedFits.remove(thing)
eos.db.commit() eos.db.commit()
self.recalc(fit) self.recalc(fit)
@@ -398,8 +417,11 @@ class Fit(object):
if m.isValidState(State.ACTIVE): if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE m.state = State.ACTIVE
# As some items may affect state-limiting attributes of the ship, calculate new attributes first
self.recalc(fit) self.recalc(fit)
# Then, check states of all modules and change where needed. This will recalc if needed
self.checkStates(fit, m) self.checkStates(fit, m)
fit.fill() fit.fill()
eos.db.commit() eos.db.commit()
@@ -857,7 +879,10 @@ class Fit(object):
if drone.amountActive > 0 and not drone.canBeApplied(fit): if drone.amountActive > 0 and not drone.canBeApplied(fit):
drone.amountActive = 0 drone.amountActive = 0
changed = True changed = True
return changed
# If any state was changed, recalculate attributes again
if changed:
self.recalc(fit)
def toggleModulesState(self, fitID, base, modules, click): def toggleModulesState(self, fitID, base, modules, click):
proposedState = self.__getProposedState(base, click) proposedState = self.__getProposedState(base, click)
@@ -873,11 +898,8 @@ class Fit(object):
# As some items may affect state-limiting attributes of the ship, calculate new attributes first # As some items may affect state-limiting attributes of the ship, calculate new attributes first
self.recalc(fit) self.recalc(fit)
# Then, check states of all modules and change where needed # Then, check states of all modules and change where needed. This will recalc if needed
changed = self.checkStates(fit, base) self.checkStates(fit, base)
# If any state was changed, recalulate attributes again
if changed is True:
self.recalc(fit)
# Old state : New State # Old state : New State
localMap = {State.OVERHEATED: State.ACTIVE, localMap = {State.OVERHEATED: State.ACTIVE,
@@ -890,7 +912,7 @@ class Fit(object):
State.ONLINE: State.ACTIVE} # Just in case State.ONLINE: State.ACTIVE} # Just in case
def __getProposedState(self, mod, click, proposedState=None): def __getProposedState(self, mod, click, proposedState=None):
if mod.slot in (Slot.RIG, Slot.SUBSYSTEM) or mod.isEmpty: if mod.slot is Slot.SUBSYSTEM or mod.isEmpty:
return State.ONLINE return State.ONLINE
currState = mod.state currState = mod.state
@@ -920,6 +942,7 @@ class Fit(object):
self.recalc(fit) self.recalc(fit)
def recalc(self, fit, withBoosters=False): def recalc(self, fit, withBoosters=False):
logger.debug("="*10+"recalc"+"="*10)
if fit.factorReload is not self.serviceFittingOptions["useGlobalForceReload"]: if fit.factorReload is not self.serviceFittingOptions["useGlobalForceReload"]:
fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"] fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"]
fit.clear() fit.clear()

View File

@@ -71,6 +71,7 @@ class ShipBrowserWorkerThread(threading.Thread):
class PriceWorkerThread(threading.Thread): class PriceWorkerThread(threading.Thread):
def run(self): def run(self):
self.queue = Queue.Queue() self.queue = Queue.Queue()
self.wait = {}
self.processUpdates() self.processUpdates()
def processUpdates(self): def processUpdates(self):
@@ -86,9 +87,21 @@ class PriceWorkerThread(threading.Thread):
wx.CallAfter(callback) wx.CallAfter(callback)
queue.task_done() 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): def trigger(self, prices, callbacks):
self.queue.put((callbacks, prices)) 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): class SearchWorkerThread(threading.Thread):
def run(self): def run(self):
self.cv = threading.Condition() self.cv = threading.Condition()
@@ -218,6 +231,8 @@ class Market():
"Mobile Decoy Unit": False, # Seems to be left over test mod for deployables "Mobile Decoy Unit": False, # Seems to be left over test mod for deployables
"Tournament Micro Jump Unit": False, # Normally seen only on tournament arenas "Tournament Micro Jump Unit": False, # Normally seen only on tournament arenas
"Council Diplomatic Shuttle": False, # CSM X celebration "Council Diplomatic Shuttle": False, # CSM X celebration
"Imp": False, # AT13 prize, not a real ship yet
"Fiend": False, # AT13 prize, not a real ship yet
} }
# do not publish ships that we convert # do not publish ships that we convert
@@ -690,10 +705,6 @@ class Market():
self.priceCache[typeID] = price self.priceCache[typeID] = price
if not price.isValid:
# if the price has expired
price.price = None
return price return price
def getPricesNow(self, typeIDs): def getPricesNow(self, typeIDs):
@@ -716,6 +727,21 @@ class Market():
self.priceWorkerThread.trigger(requests, cb) 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:
pass
self.priceWorkerThread.setToWait(item.ID, cb)
def clearPriceCache(self): def clearPriceCache(self):
self.priceCache.clear() self.priceCache.clear()
deleted_rows = eos.db.clearPrices() deleted_rows = eos.db.clearPrices()

View File

@@ -55,4 +55,4 @@ else:
# If database does not exist, do not worry about migration. Simply # If database does not exist, do not worry about migration. Simply
# create and set version # create and set version
eos.db.saveddata_meta.create_all() eos.db.saveddata_meta.create_all()
eos.db.saveddata_engine.execute('PRAGMA user_version = %d'%config.dbversion) eos.db.saveddata_engine.execute('PRAGMA user_version = {}'.format(migration.getAppVersion()))

Some files were not shown because too many files have changed in this diff Show More