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
#### 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
* [Development repository: http://github.com/DarkFenX/Pyfa](http://github.com/DarkFenX/Pyfa)
* [XMPP conference:

View File

@@ -1,6 +1,11 @@
import os
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
try:
import configforced
@@ -12,16 +17,14 @@ debug = False
# Defines if our saveddata will be in pyfa root or not
saveInRoot = False
# Version data
version = "1.11.1"
tag = "git"
expansionName = "Singularity"
expansionVersion = "883859"
evemonMinVersion = "4081"
logLevel = logging.DEBUG
# Database version (int ONLY)
# Increment every time we need to flag for user database upgrade/modification
dbversion = 7
# Version data
version = "1.13.3"
tag = "git"
expansionName = "Aegis"
expansionVersion = "1.0"
evemonMinVersion = "4081"
pyfaPath = None
savePath = None
@@ -29,10 +32,24 @@ staticPath = None
saveDB = 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)
import logging
logging.basicConfig()
class StreamToLogger(object):
"""
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():
global pyfaPath
@@ -59,19 +76,25 @@ def defPaths():
savePath = unicode(os.path.expanduser(os.path.join("~", ".pyfa")),
sys.getfilesystemencoding())
# Redirect stderr to file if we're requested to do so
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")
__createDirs(savePath)
# Same for stdout
stdoutToFile = getattr(configforced, "stdoutToFile", None)
if stdoutToFile is True:
if not os.path.exists(savePath):
os.mkdir(savePath)
sys.stdout = open(os.path.join(savePath, "output_log.txt"), "w")
format = '%(asctime)s %(name)-24s %(levelname)-8s %(message)s'
logging.basicConfig(format=format, level=logLevel)
handler = logging.handlers.RotatingFileHandler(os.path.join(savePath, "log.txt"), maxBytes=1000000, backupCount=3)
formatter = logging.Formatter(format)
handler.setFormatter(formatter)
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
# directory in our pyfa directory

View File

@@ -1,32 +1,46 @@
import config
import shutil
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):
cursor = db.execute('PRAGMA user_version')
return cursor.fetchone()[0]
def update(saveddata_engine):
currversion = getVersion(saveddata_engine)
dbVersion = getVersion(saveddata_engine)
appVersion = getAppVersion()
if currversion == config.dbversion:
if dbVersion == appVersion:
return
if currversion < config.dbversion:
if dbVersion < appVersion:
# Automatically backup database
toFile = "%s/saveddata_migration_%d-%d_%s.db"%(
config.savePath,
currversion,
config.dbversion,
dbVersion,
appVersion,
time.strftime("%Y%m%d_%H%M%S"))
shutil.copyfile(config.saveDB, toFile)
for version in xrange(currversion, config.dbversion):
module = __import__('eos.db.migrations.upgrade%d'%(version+1), fromlist=True)
for version in xrange(dbVersion, appVersion):
module = __import__("eos.db.migrations.upgrade{}".format(version + 1), fromlist=True)
upgrade = getattr(module, "upgrade", False)
if upgrade:
upgrade(saveddata_engine)
# 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("fitID", Integer, ForeignKey("fits.ID"), nullable = False),
Column("active", Boolean),
UniqueConstraint("itemID", "fitID"))
)
activeSideEffects_table = Table("boostersActiveSideEffects", saveddata_meta,
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/>.
#===============================================================================
from sqlalchemy import Table, Column, Integer, ForeignKey, String, Boolean
from sqlalchemy.orm import relation, mapper
from sqlalchemy import *
from sqlalchemy.orm import *
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.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.implant import fitImplants_table
from eos.types import Fit, Module, User, Booster, Drone, Cargo, Implant, Character, DamagePattern, TargetResists
from eos.effectHandlerHelpers import HandledModuleList, HandledDroneList, \
HandledImplantBoosterList, HandledProjectedModList, HandledProjectedDroneList, \
HandledProjectedFitList, HandledCargoList
from eos.effectHandlerHelpers import *
fits_table = Table("fits", saveddata_meta,
Column("ID", Integer, primary_key = True),
@@ -47,31 +47,100 @@ fits_table = Table("fits", saveddata_meta,
projectedFits_table = Table("projectedFits", saveddata_meta,
Column("sourceID", 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,
properties = {"_Fit__modules" : relation(Module, collection_class = HandledModuleList,
primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False),
order_by = modules_table.c.position, cascade='all, delete, delete-orphan'),
"_Fit__projectedModules" : relation(Module, collection_class = HandledProjectedModList, cascade='all, delete, delete-orphan', single_parent=True,
primaryjoin = and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)),
"owner" : relation(User, backref = "fits"),
"_Fit__boosters" : relation(Booster, collection_class = HandledImplantBoosterList, cascade='all, delete, delete-orphan', single_parent=True),
"_Fit__drones" : relation(Drone, collection_class = HandledDroneList, 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 = HandledCargoList, 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', 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),
"_Fit__projectedFits" : relation(Fit,
primaryjoin = projectedFits_table.c.victimID == fits_table.c.ID,
secondaryjoin = fits_table.c.ID == projectedFits_table.c.sourceID,
secondary = projectedFits_table,
collection_class = HandledProjectedFitList)
})
properties = {
"_Fit__modules": relation(
Module,
collection_class=HandledModuleList,
primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == False),
order_by=modules_table.c.position,
cascade='all, delete, delete-orphan'),
"_Fit__projectedModules": relation(
Module,
collection_class=HandledProjectedModList,
cascade='all, delete, delete-orphan',
single_parent=True,
primaryjoin=and_(modules_table.c.fitID == fits_table.c.ID, modules_table.c.projected == True)),
"owner": relation(
User,
backref="fits"),
"itemID": fits_table.c.shipID,
"shipID": fits_table.c.shipID,
"_Fit__boosters": relation(
Booster,
collection_class=HandledImplantBoosterList,
cascade='all, delete, delete-orphan',
single_parent=True),
"_Fit__drones": relation(
Drone,
collection_class=HandledDroneCargoList,
cascade='all, delete, delete-orphan',
single_parent=True,
primaryjoin=and_(drones_table.c.fitID == fits_table.c.ID, drones_table.c.projected == False)),
"_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()
else:
raise TypeError("Need integer as argument")
if fit and fit.isInvalid:
with sd_lock:
removeInvalid([fit])
return None
return fit
@cachedQuery(Fleet, 1, "fleetID")
@@ -244,9 +250,10 @@ def getFitsWithShip(shipID, ownerID=None, where=None, eager=None):
filter = processWhere(filter, where)
eager = processEager(eager)
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:
raise TypeError("ShipID must be integer")
return fits
def getBoosterFits(ownerID=None, where=None, eager=None):
@@ -264,7 +271,8 @@ def getBoosterFits(ownerID=None, where=None, eager=None):
filter = processWhere(filter, where)
eager = processEager(eager)
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
def countAllFits():
@@ -295,7 +303,8 @@ def countFitsWithShip(shipID, ownerID=None, where=None, eager=None):
def getFitList(eager=None):
eager = processEager(eager)
with sd_lock:
fits = saveddata_session.query(Fit).options(*eager).all()
fits = removeInvalid(saveddata_session.query(Fit).options(*eager).all())
return fits
def getFleetList(eager=None):
@@ -385,7 +394,8 @@ def searchFits(nameLike, where=None, eager=None):
filter = processWhere(Fit.name.like(nameLike, escape="\\"), where)
eager = processEager(eager)
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
def getSquadsIDsWithFitID(fitID):
@@ -406,6 +416,16 @@ def getProjectedFits(fitID):
else:
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):
with sd_lock:
saveddata_session.add(stuff)

View File

@@ -17,8 +17,12 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
#from sqlalchemy.orm.attributes import flag_modified
import eos.db
import eos.types
import logging
logger = logging.getLogger(__name__)
class HandledList(list):
def filteredItemPreAssign(self, filter, *args, **kwargs):
@@ -101,6 +105,14 @@ class HandledList(list):
except AttributeError:
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):
def append(self, mod):
emptyPosition = float("Inf")
@@ -115,10 +127,14 @@ class HandledModuleList(HandledList):
del self[emptyPosition]
mod.position = emptyPosition
HandledList.insert(self, emptyPosition, mod)
if mod.isInvalid:
self.remove(mod)
return
mod.position = len(self)
HandledList.append(self, mod)
if mod.isInvalid:
self.remove(mod)
def insert(self, index, mod):
mod.position = index
@@ -149,133 +165,72 @@ class HandledModuleList(HandledList):
if mod.getModifiedItemAttr("subSystemSlot") == slot:
del self[i]
class HandledDroneList(HandledList):
class HandledDroneCargoList(HandledList):
def find(self, item):
for d in self:
if d.item == item:
yield d
for o in self:
if o.item == item:
yield o
def findFirst(self, item):
for d in self.find(item):
return d
for o in self.find(item):
return o
def append(self, drone):
list.append(self, drone)
def append(self, thing):
HandledList.append(self, thing)
def remove(self, drone):
HandledList.remove(self, drone)
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
if thing.isInvalid:
self.remove(thing)
class HandledImplantBoosterList(HandledList):
def __init__(self):
self.__slotCache = {}
def append(self, thing):
if thing.isInvalid:
HandledList.append(self, thing)
self.remove(thing)
return
def append(self, implant):
if self.__slotCache.has_key(implant.slot):
raise ValueError("Implant/Booster slot already in use, remove the old one first or set replace = True")
self.__slotCache[implant.slot] = implant
HandledList.append(self, implant)
# if needed, remove booster that was occupying slot
oldObj = next((m for m in self if m.slot == thing.slot), None)
if oldObj:
logging.info("Slot %d occupied with %s, replacing with %s", thing.slot, oldObj.item.name, thing.item.name)
oldObj.itemID = 0 # hack to remove from DB. See GH issue #324
self.remove(oldObj)
def remove(self, implant):
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
HandledList.append(self, thing)
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):
proj.projected = True
HandledList.append(self, proj)
class HandledProjectedDroneList(HandledDroneList):
def append(self, proj):
proj.projected = True
list.append(self, proj)
class HandledProjectedFitList(HandledList):
def append(self, proj):
proj.projected = True
list.append(self, proj)
# Remove invalid or non-projectable drones
if proj.isInvalid or not proj.item.isType("projected"):
self.remove(proj)
class HandledItem(object):
def preAssignItemAttr(self, *args, **kwargs):

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
# eliteBonusHeavyInterdictorsWarpDisruptFieldGeneratorWarpScrambleRange2
#
# Used by:
# Ships from group: Heavy Interdiction Cruiser (4 of 4)
# Ships from group: Heavy Interdiction Cruiser (4 of 5)
type = "passive"
def handler(fit, ship, context):
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
#
# Used by:
# Ships from group: Interceptor (5 of 9)
# Ships from group: Interceptor (5 of 10)
type = "passive"
def handler(fit, ship, context):
level = fit.character.getSkill("Interceptors").level

View File

@@ -1,7 +1,7 @@
# interceptorMWDSignatureRadiusBonus
#
# Used by:
# Ships from group: Interceptor (9 of 9)
# Ships from group: Interceptor (9 of 10)
type = "passive"
def handler(fit, ship, context):
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"
def handler(fit, container, context):
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"),
"aoeCloudSize", container.getModifiedItemAttr("aoeCloudSizeBonus") * level,
stackingPenalties = False)
stackingPenalties=penalize)

View File

@@ -7,6 +7,7 @@
type = "passive"
def handler(fit, container, context):
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"),
"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"
def handler(fit, container, context):
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
#
# Used by:
# Modules named like: Propulsion Mode (3 of 3)
# Modules named like: Propulsion Mode (4 of 4)
type = "passive"
def handler(fit, module, context):
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
#
# Used by:
# Module: Confessor Defense Mode
# Module: Svipul Defense Mode
# Modules named like: Defense Mode (3 of 4)
type = "passive"
def handler(fit, module, context):
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
#
# Used by:
# Modules named like: Propulsion Mode (3 of 3)
# Modules named like: Propulsion Mode (3 of 4)
type = "passive"
def handler(fit, module, context):
fit.ship.multiplyItemAttr(

View File

@@ -1,6 +1,7 @@
# OffensiveDefensiveReduction
#
# Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Incursion ship attributes effects (3 of 3)
runTime = "early"
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
#
# Used by:
# Ships from group: Tactical Destroyer (3 of 3)
# Ships from group: Tactical Destroyer (4 of 4)
type = "passive"
def handler(fit, ship, context):
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:
# Ship: Daredevil
# Ship: Hecate
type = "passive"
def handler(fit, ship, context):
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
#
# Used by:
# Ships from group: Interceptor (9 of 9)
# Ships from group: Interceptor (9 of 10)
# Ship: Atron
# Ship: Condor
# 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
#
# Used by:
# Modules named like: Sharpshooter Mode (3 of 3)
# Modules named like: Sharpshooter Mode (4 of 4)
type = "passive"
def handler(fit, module, context):
fit.ship.multiplyItemAttr(

View File

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

View File

@@ -1,7 +1,7 @@
# shipModeScanStrengthPostDiv
#
# Used by:
# Modules named like: Sharpshooter Mode (3 of 3)
# Modules named like: Sharpshooter Mode (4 of 4)
type = "passive"
def handler(fit, module, context):
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
#
# Used by:
# Celestials named like: Drifter Incursion (6 of 6)
# Celestials named like: Magnetar Effect Beacon Class (6 of 6)
runTime = "early"
type = ("projected", "offline")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@
# Used by:
# Module: Triage Module II
type = "active"
runTime = "early"
def handler(fit, module, context):
# Remote armor reps
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.effectHandlerHelpers import HandledItem
from sqlalchemy.orm import reconstructor, validates
import eos.db
import logging
logger = logging.getLogger(__name__)
class Booster(HandledItem, ItemAttrShortcut):
def __init__(self, item):
self.__slot = self.__calculateSlot(item)
self.itemID = item.ID
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.build()
@reconstructor
def init(self):
"""Initialize a booster 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 Booser", self.itemID)
return
self.build()
def build(self):
""" Build object. Assumes proper and valid item already set """
self.__sideEffects = []
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes
self.__sideEffects = []
for effect in self.item.effects.itervalues():
self.__slot = self.__calculateSlot(self.__item)
for effect in self.__item.effects.itervalues():
if effect.isType("boosterSideEffect"):
s = SideEffect(self)
s.effect = effect
s.active = effect.ID in self.__activeSideEffectIDs
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):
return self.__sideEffects.__iter__()
@@ -62,23 +79,18 @@ class Booster(HandledItem, ItemAttrShortcut):
@property
def itemModifiedAttributes(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__itemModifiedAttributes
@property
def slot(self):
if self.__item is None:
self.__fetchItemInfo()
def isInvalid(self):
return self.__item is None or self.__item.group.name != "Booster"
@property
def slot(self):
return self.__slot
@property
def item(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__item
def __calculateSlot(self, item):

View File

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

View File

@@ -192,7 +192,7 @@ class Character(object):
map = {"ID": lambda val: isinstance(val, int),
"name" : lambda val: True,
"apiKey" : lambda val: val is None or (isinstance(val, basestring) and len(val) > 0),
"ownerID" : lambda val: isinstance(val, int)}
"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)
else: return val

View File

@@ -20,81 +20,80 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut, ChargeAttrShortcut
from eos.effectHandlerHelpers import HandledItem, HandledCharge
from sqlalchemy.orm import validates, reconstructor
import eos.db
import logging
logger = logging.getLogger(__name__)
class Drone(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
DAMAGE_TYPES = ("em", "kinetic", "explosive", "thermal")
MINING_ATTRIBUTES = ("miningAmount",)
def __init__(self, item):
if item.category.name != "Drone":
raise ValueError("Passed item is not a drone")
"""Initialize a drone from the program"""
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.amountActive = 0
self.__dps = None
self.__volley = None
self.__miningyield = None
self.projected = False
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.itemModifiedAttributes.original = self.item.attributes
self.build()
@reconstructor
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.__volley = 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.original = self.item.attributes
self.__itemModifiedAttributes.original = self.__item.attributes
def __fetchChargeInfo(self):
chargeID = self.getModifiedItemAttr("entityMissileTypeID")
self.__chargeModifiedAttributes = ModifiedAttributeDict()
chargeID = self.getModifiedItemAttr("entityMissileTypeID")
if chargeID is not None:
import eos.db
charge = eos.db.getItem(int(chargeID))
self.__charge = charge
self.chargeModifiedAttributes.original = charge.attributes
else:
self.__charge = 0
self.__chargeModifiedAttributes.original = charge.attributes
@property
def itemModifiedAttributes(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__itemModifiedAttributes
@property
def chargeModifiedAttributes(self):
if self.__charge is None:
self.__fetchChargeInfo()
return self.__chargeModifiedAttributes
@property
def item(self):
if self.__item is None:
self.__fetchItemInfo()
def isInvalid(self):
return self.__item is None or self.__item.category.name != "Drone"
@property
def item(self):
return self.__item
@property
def charge(self):
if self.__charge is None:
self.__fetchChargeInfo()
return self.__charge if self.__charge != 0 else None
return self.__charge
@property
def dealsDamage(self):

View File

@@ -17,8 +17,7 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
#===============================================================================
from eos.effectHandlerHelpers import HandledList, HandledModuleList, HandledDroneList, HandledImplantBoosterList, \
HandledProjectedFitList, HandledProjectedModList, HandledProjectedDroneList, HandledCargoList
from eos.effectHandlerHelpers import *
from eos.modifiedAttributeDict import ModifiedAttributeDict
from sqlalchemy.orm import validates, reconstructor
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.saveddata.module import State
from eos.saveddata.mode import Mode
import eos.db
import time
import copy
from utils.timer import Timer
import logging
logger = logging.getLogger(__name__)
try:
from collections import OrderedDict
@@ -48,34 +54,57 @@ class Fit(object):
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.__drones = HandledDroneList()
self.__cargo = HandledCargoList()
self.__drones = HandledDroneCargoList()
self.__cargo = HandledDroneCargoList()
self.__implants = HandledImplantBoosterList()
self.__boosters = HandledImplantBoosterList()
self.__projectedFits = HandledProjectedFitList()
#self.__projectedFits = {}
self.__projectedModules = HandledProjectedModList()
self.__projectedDrones = HandledProjectedDroneList()
self.__character = None
self.__owner = None
self.shipID = None
self.projected = False
self.name = ""
self.fleet = None
self.boostsFits = set()
self.gangBoosts = None
self.name = name
self.timestamp = time.time()
self.ecmProjectedStr = 1
self.modeID = None
self.build()
@reconstructor
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()
def build(self):
from eos import db
self.__extraDrains = []
self.__ehp = None
self.__weaponDPS = None
@@ -100,11 +129,6 @@ class Fit(object):
self.ecmProjectedStr = 1
self.extraAttributes = ModifiedAttributeDict(self)
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
def targetResists(self):
@@ -128,13 +152,17 @@ class Fit(object):
self.__ehp = None
self.__effectiveTank = None
@property
def isInvalid(self):
return self.__ship is None
@property
def mode(self):
return self._mode
return self.__mode
@mode.setter
def mode(self, mode):
self._mode = mode
self.__mode = mode
self.modeID = mode.item.ID if mode is not None else None
@property
@@ -154,7 +182,7 @@ class Fit(object):
self.__ship = ship
self.shipID = ship.item.ID if ship is not None else None
# 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
def drones(self):
@@ -182,7 +210,12 @@ class Fit(object):
@property
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
def projectedDrones(self):
@@ -295,13 +328,13 @@ class Fit(object):
@validates("ID", "ownerID", "shipID")
def validator(self, key, val):
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}
if map[key](val) == False: raise ValueError(str(val) + " is not a valid value for " + key)
else: return val
def clear(self):
def clear(self, projected=False):
self.__effectiveTank = None
self.__weaponDPS = None
self.__minerYield = None
@@ -321,10 +354,30 @@ class Fit(object):
del self.__calculatedTargets[:]
del self.__extraDrains[:]
if self.ship is not None: self.ship.clear()
c = chain(self.modules, self.drones, self.boosters, self.implants, self.projectedDrones, self.projectedModules, self.projectedFits, (self.character, self.extraAttributes))
if self.ship:
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:
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,
#so we can correctly map "Affected By"
@@ -338,7 +391,48 @@ class Fit(object):
def getModifier(self):
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):
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
if withBoosters is True:
refreshBoosts = True
@@ -347,6 +441,7 @@ class Fit(object):
if dirtyStorage is not None:
dirtyStorage.update(self.boostsFits)
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)
elif self.fleet is None:
self.gangBoosts = None
@@ -355,80 +450,64 @@ class Fit(object):
dirtyStorage.remove(self.ID)
except KeyError:
pass
# If we're not explicitly asked to project fit onto something,
# set self as target fit
if targetFit is None:
targetFit = self
forceProjected = 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
projected = False
else:
return
projected = True
# 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
# Mark fit as calculated
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"):
# Build a little chain of stuff
# Avoid adding projected drones and modules when fit is projected onto self
# TODO: remove this workaround when proper self-projection using virtual duplicate fits is implemented
if forceProjected is True:
c = chain((self.character, self.ship, self.mode), self.drones, self.boosters, self.appliedImplants, self.modules)
else:
c = chain((self.character, self.ship, self.mode), self.drones, self.boosters, self.appliedImplants, self.modules,
self.projectedDrones, self.projectedModules)
c = chain(
(self.character, self.ship),
self.drones,
self.boosters,
self.appliedImplants,
self.modules
)
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:
contextMap = {Skill: "skill",
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
self.__calculateGangBoosts(runTime)
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:
self.register(item)
item.calculateModifiedAttributes(self, runTime, False)
if forceProjected is True:
targetFit.register(item)
item.calculateModifiedAttributes(targetFit, runTime, True)
if projected is True:
for _ in xrange(projectionInfo.amount):
targetFit.register(item)
item.calculateModifiedAttributes(targetFit, runTime, True)
for fit in self.projectedFits:
fit.calculateModifiedAttributes(self, withBoosters=withBoosters, dirtyStorage=dirtyStorage)
timer.checkpoint('Done with runtime: %s'%runTime)
# 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):
"""
@@ -901,6 +980,9 @@ class Fit(object):
c.append(deepcopy(i, memo))
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

View File

@@ -20,46 +20,58 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut
from eos.effectHandlerHelpers import HandledItem
from sqlalchemy.orm import validates, reconstructor
import eos.db
import logging
logger = logging.getLogger(__name__)
class Implant(HandledItem, ItemAttrShortcut):
def __init__(self, item):
self.__slot = self.__calculateSlot(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.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.item.attributes
self.build()
@reconstructor
def init(self):
self.__item = None
def __fetchItemInfo(self):
import eos.db
self.__item = eos.db.getItem(self.itemID)
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 an Implant", self.itemID)
return
self.build()
def build(self):
""" Build object. Assumes proper and valid item already set """
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__itemModifiedAttributes.original = self.__item.attributes
self.__slot = self.__calculateSlot(self.__item)
@property
def itemModifiedAttributes(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__itemModifiedAttributes
@property
def slot(self):
if self.__item is None:
self.__fetchItemInfo()
def isInvalid(self):
return self.__item is None or self.__item.category.name != "Implant"
@property
def slot(self):
return self.__slot
@property
def item(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__item
def __calculateSlot(self, item):

View File

@@ -19,36 +19,24 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut
from eos.effectHandlerHelpers import HandledItem
import eos.db
class Mode(ItemAttrShortcut, HandledItem):
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.__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
@property
def item(self):
if isinstance(self.__item, int):
self.__fetchItemInfo()
return self.__item
@property
def itemModifiedAttributes(self):
if isinstance(self.__item, int):
self.__fetchItemInfo()
return self.__itemModifiedAttributes
# @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.enum import Enum
from eos.mathUtils import floorFloat
import eos.db
import logging
logger = logging.getLogger(__name__)
class State(Enum):
OFFLINE = -1
@@ -49,95 +53,78 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
MINING_ATTRIBUTES = ("miningAmount", )
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.__charge = 0
self.projected = False
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.__miningyield = None
self.__volley = None
self.__reloadTime = None
self.__reloadForce = None
self.__chargeCycles = None
self.__hardpoint = Hardpoint.NONE
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.__slot = self.dummySlot # defaults to None
@reconstructor
def init(self):
if self.dummySlot is None:
self.__item = None
self.__charge = None
self.__volley = None
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
if self.__item:
self.__itemModifiedAttributes.original = self.__item.attributes
self.__hardpoint = self.__calculateHardpoint(self.__item)
self.__slot = self.__calculateSlot(self.__item)
if self.__charge:
self.__chargeModifiedAttributes.original = self.__charge.attributes
@classmethod
def buildEmpty(cls, slot):
empty = Module(None)
empty.__slot = slot
empty.__hardpoint = Hardpoint.NONE
empty.__item = 0
empty.__charge = 0
empty.dummySlot = slot
empty.__itemModifiedAttributes = ModifiedAttributeDict()
empty.__chargeModifiedAttributes = ModifiedAttributeDict()
return empty
@classmethod
def buildRack(cls, slot):
empty = Rack(None)
empty.__slot = slot
empty.__hardpoint = Hardpoint.NONE
empty.__item = 0
empty.__charge = 0
empty.dummySlot = slot
empty.__itemModifiedAttributes = ModifiedAttributeDict()
empty.__chargeModifiedAttributes = ModifiedAttributeDict()
return empty
@property
@@ -146,11 +133,14 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@property
def hardpoint(self):
if self.__item is None:
self.__fetchItemInfo()
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
def numCharges(self):
if self.charge is None:
@@ -263,38 +253,23 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
@property
def slot(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__slot
@property
def itemModifiedAttributes(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__itemModifiedAttributes
@property
def chargeModifiedAttributes(self):
if self.__charge is None:
self.__fetchChargeInfo()
return self.__chargeModifiedAttributes
@property
def item(self):
if self.__item is None:
self.__fetchItemInfo()
return self.__item if self.__item != 0 else None
@property
def charge(self):
if self.__charge is None:
self.__fetchChargeInfo()
return self.__charge if self.__charge != 0 else None
@charge.setter
@@ -516,7 +491,6 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
def getValidCharges(self):
validCharges = set()
import eos.db
for i in range(5):
itemChargeGroup = self.getModifiedItemAttr('chargeGroup' + str(i))
if itemChargeGroup is not None:

View File

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

View File

@@ -20,6 +20,10 @@
from eos.modifiedAttributeDict import ModifiedAttributeDict, ItemAttrShortcut
from eos.effectHandlerHelpers import HandledItem
from eos.saveddata.mode import Mode
import eos.db
import logging
logger = logging.getLogger(__name__)
class Ship(ItemAttrShortcut, HandledItem):
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))
self.__item = item
self.__modeItems = self.__getModeItems()
self.__itemModifiedAttributes = ModifiedAttributeDict()
self.__modeItems = self._getModeItems()
if not isinstance(item, int):
self.__buildOriginal()
self.__itemModifiedAttributes.original = self.item.attributes
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
def item(self):
if isinstance(self.__item, int):
self.__fetchItemInfo()
return self.__item
@property
def itemModifiedAttributes(self):
if isinstance(self.__item, int):
self.__fetchItemInfo()
return self.__itemModifiedAttributes
def clear(self):
@@ -67,25 +56,16 @@ class Ship(ItemAttrShortcut, HandledItem):
if effect.runTime == runTime and effect.isType("passive"):
effect.handler(fit, self, ("ship",))
def checkModeItem(self, item):
"""
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
"""
def validateModeItem(self, item):
""" Checks if provided item is a valid mode """
items = self.__modeItems
if items != None:
if item == None or item not in items:
# We have a tact dessy, but mode is None or not valid. Force new mode
if items is not None:
# if we have items, then we are in a tactical destroyer and must have a mode
if item is None or item not in items:
# If provided item is invalid mode, force new one
return Mode(items[0])
elif item in items:
# We have a valid mode
return Mode(item)
return Mode(item)
return None
@property
@@ -96,21 +76,17 @@ class Ship(ItemAttrShortcut, HandledItem):
def modes(self):
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
valid Item objects, not the Mode objects. Returns None if not a
t3 dessy
"""
# @todo: is there a better way to determine this that isn't hardcoded groupIDs?
if self.item.groupID != 1305:
if self.item.group.name != "Tactical Destroyer":
return None
modeGroupID = 1306
import eos.db
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:
# Rely on name detection because race is not reliable
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)
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):
if location in locationMap:
if location == "pack":
location = locationMap[location]
name = stripPath(name)
filename = "icon{0}.png".format(name)
path = os.path.join(location, filename)
else:

View File

@@ -17,5 +17,6 @@ __all__ = [
#"changeAffectingSkills",
"tacticalMode",
"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))
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 )
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 )
self.sFit = service.Fit.getInstance()
@@ -69,6 +72,7 @@ class PFGeneralPref ( PreferenceView):
self.cbCompactSkills.SetValue(self.sFit.serviceFittingOptions["compactSkills"] or False)
self.cbReopenFits.SetValue(self.openFitsSettings["enabled"])
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.cbGlobalDmgPattern.Bind(wx.EVT_CHECKBOX, self.OnCBGlobalDmgPatternStateChange)
@@ -79,6 +83,7 @@ class PFGeneralPref ( PreferenceView):
self.cbCompactSkills.Bind(wx.EVT_CHECKBOX, self.onCBCompactSkills)
self.cbReopenFits.Bind(wx.EVT_CHECKBOX, self.onCBReopenFits)
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)
@@ -135,6 +140,9 @@ class PFGeneralPref ( PreferenceView):
def onCBShowTooltip(self, event):
self.sFit.serviceFittingOptions["showTooltip"] = self.cbShowTooltip.GetValue()
def onCBShowShortcuts(self, event):
self.sFit.serviceFittingOptions["showMarketShortcuts"] = self.cbMarketShortcuts.GetValue()
def getImage(self):
return bitmapLoader.getBitmap("prefs_settings", "icons")

View File

@@ -30,37 +30,13 @@ class PriceViewFull(StatsView):
def __init__(self, parent):
StatsView.__init__(self)
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._cachedFittings = 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):
return "Price"
def getTextExtentW(self, text):
width, height = self.parent.GetTextExtent(text)
return width
def populatePanel(self, contentPanel, headerPanel):
contentSizer = contentPanel.GetSizer()
self.panel = contentPanel
@@ -111,22 +87,11 @@ class PriceViewFull(StatsView):
for cargo in fit.cargo:
for _ in xrange(cargo.amount):
typeIDs.append(cargo.itemID)
if self._timer:
if self._timer.IsRunning():
self._timer.Stop()
sMkt = service.Market.getInstance()
sMkt.getPrices(typeIDs, self.processPrices)
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:
if self._timer:
if self._timer.IsRunning():
self._timer.Stop()
self.labelEMStatus.SetLabel("")
self.labelPriceShip.SetLabel("0.0 ISK")
self.labelPriceFittings.SetLabel("0.0 ISK")
@@ -136,20 +101,10 @@ class PriceViewFull(StatsView):
def processPrices(self, prices):
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:]))
self.labelEMStatus.SetLabel("")
if self._cachedShip != shipPrice:
self.labelPriceShip.SetLabel("%s ISK" % formatAmount(shipPrice, 3, 3, 9, currency=True))
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())
if ehp is not None:
total += ehp[tankType]
rrFactor = fit.ehp[tankType] / fit.hp[tankType]
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:
lbl.SetLabel("0")

View File

@@ -21,6 +21,8 @@
from gui import builtinViewColumns
from gui.viewColumn import ViewColumn
from gui import bitmapLoader
import gui.mainFrame
import wx
from eos.types import Drone, Cargo, Fit, Module, Slot, Rack
import service
@@ -29,6 +31,7 @@ class BaseName(ViewColumn):
name = "Base Name"
def __init__(self, fittingView, params):
ViewColumn.__init__(self, fittingView)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.columnText = "Name"
self.shipImage = fittingView.imageList.GetImageIndex("ship_small", "icons")
self.mask = wx.LIST_MASK_TEXT
@@ -39,7 +42,8 @@ class BaseName(ViewColumn):
elif isinstance(stuff, Cargo):
return "%dx %s" % (stuff.amount, stuff.item.name)
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):
if service.Fit.getInstance().serviceFittingOptions["rackLabels"]:
if stuff.slot == Slot.MODE:
@@ -55,6 +59,15 @@ class BaseName(ViewColumn):
return stuff.item.name
else:
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
BaseName.register()

View File

@@ -33,13 +33,13 @@ class Price(ViewColumn):
self.imageId = fittingView.imageList.GetImageIndex("totalPrice_small", "icons")
def getText(self, stuff):
if stuff.item is None:
if stuff.item is None or stuff.item.group.name == "Ship Modifiers":
return ""
sMkt = service.Market.getInstance()
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
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)
def delayedText(self, mod, display, colItem):
def callback(requests):
price = requests[0].price
colItem.SetText(formatAmount(price, 3, 3, 9, currency=True) if price else "")
sMkt = service.Market.getInstance()
def callback(item):
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)
service.Market.getInstance().getPrices([mod.item.ID], callback)
sMkt.waitForPrice(mod.item, callback)
def getImageId(self, mod):
return -1

View File

@@ -19,27 +19,21 @@
from gui.viewColumn import ViewColumn
from gui import bitmapLoader
import gui.mainFrame
import wx
from eos.types import Drone, Module, Rack
from eos.types import Drone, Module, Rack, Fit
from eos.types import State as State_
class State(ViewColumn):
name = "State"
def __init__(self, fittingView, params):
ViewColumn.__init__(self, fittingView)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.resizable = False
self.size = 16
self.maxsize = self.size
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):
return ""
@@ -49,8 +43,14 @@ class State(ViewColumn):
return State_.getName(mod.state).title()
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):
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):
return -1
elif isinstance(stuff, Module):
@@ -58,11 +58,21 @@ class State(ViewColumn):
return -1
else:
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:
active = getattr(stuff, "active", None)
if active is None:
return -1
else:
return self.checkedId if active else self.uncheckedId
if active:
return generic_active
return generic_inactive
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.sview = SkillTreeView(self.viewsNBContainer)
self.iview = ImplantsTreeView(self.viewsNBContainer)
#self.iview = ImplantsTreeView(self.viewsNBContainer)
#=======================================================================
# RC2
self.iview.Show(False)
#self.iview.Show(False)
#=======================================================================
self.aview = APIView(self.viewsNBContainer)
@@ -147,6 +147,7 @@ class CharacterEditor(wx.Frame):
def restrict(self):
self.btnRename.Enable(False)
self.btnDelete.Enable(False)
self.aview.stDisabledTip.Show(True)
self.aview.inputID.Enable(False)
self.aview.inputKey.Enable(False)
self.aview.charChoice.Enable(False)
@@ -158,6 +159,7 @@ class CharacterEditor(wx.Frame):
def unrestrict(self):
self.btnRename.Enable(True)
self.btnDelete.Enable(True)
self.aview.stDisabledTip.Show(False)
self.aview.inputID.Enable(True)
self.aview.inputKey.Enable(True)
self.aview.btnFetchCharList.Enable(True)
@@ -272,6 +274,7 @@ class CharacterEditor(wx.Frame):
class SkillTreeView (wx.Panel):
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)
self.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
pmainSizer = wx.BoxSizer(wx.VERTICAL)
@@ -531,12 +534,23 @@ class APIView (wx.Panel):
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)
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.apiUrlKeyList = u"https://community.eveonline.com/support/api-key/"
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.AddGrowableCol(1)
fgSizerInput.SetFlexibleDirection(wx.BOTH)

View File

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

View File

@@ -36,7 +36,8 @@ class DroneViewDrop(wx.PyDropTarget):
def OnData(self, x, y, t):
if self.GetData():
self.dropFn(x, y, int(self.dropData.GetText()))
data = self.dropData.GetText().split(':')
self.dropFn(x, y, data)
return t
class DroneView(d.Display):
@@ -72,7 +73,7 @@ class DroneView(d.Display):
self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.startDrag)
self.SetDropTarget(DroneViewDrop(self.mergeDrones))
self.SetDropTarget(DroneViewDrop(self.handleDragDrop))
def OnLeaveWindow(self, event):
self.SetToolTip(None)
@@ -117,17 +118,27 @@ class DroneView(d.Display):
row = event.GetIndex()
if row != -1:
data = wx.PyTextDataObject()
data.SetText(str(self.GetItemData(row)))
data.SetText("drone:"+str(row))
dropSource = wx.DropSource(self)
dropSource.SetData(data)
res = dropSource.DoDragDrop()
def mergeDrones(self, x, y, itemID):
srcRow = self.FindItemData(-1,itemID)
dstRow, _ = self.HitTest((x, y))
if srcRow != -1 and dstRow != -1:
self._merge(srcRow, dstRow)
def handleDragDrop(self, x, y, data):
'''
Handles dragging of items from various pyfa displays which support it
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):
sFit = service.Fit.getInstance()

View File

@@ -39,7 +39,7 @@ import gui.globalEvents as GE
from gui import bitmapLoader
from gui.mainMenuBar import MainMenuBar
from gui.additionsPane import AdditionsPane
from gui.marketBrowser import MarketBrowser
from gui.marketBrowser import MarketBrowser, ItemSelected
from gui.multiSwitch import MultiSwitch
from gui.statsPane import StatsPane
from gui.shipBrowser import ShipBrowser, FitSelected, ImportSelected, Stage3Selected
@@ -146,6 +146,7 @@ class MainFrame(wx.Frame):
self.marketBrowser = MarketBrowser(self.notebookBrowsers)
self.notebookBrowsers.AddPage(self.marketBrowser, "Market", tabImage = marketImg, showClose = False)
self.marketBrowser.splitter.SetSashPosition(self.marketHeight)
self.shipBrowser = ShipBrowser(self.notebookBrowsers)
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.SetMinimumPaneSize(204)
self.splitter.SetSashPosition(300)
self.splitter.SetSashPosition(self.browserWidth)
cstatsSizer = wx.BoxSizer(wx.VERTICAL)
@@ -227,7 +228,7 @@ class MainFrame(wx.Frame):
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)
if self.mainFrameAttribs["wnd_maximized"]:
@@ -241,6 +242,9 @@ class MainFrame(wx.Frame):
self.SetSize((width, height))
self.SetMinSize((mainFrameDefaultAttribs["wnd_width"], mainFrameDefaultAttribs["wnd_height"]))
self.browserWidth = self.mainFrameAttribs["browser_width"]
self.marketHeight = self.mainFrameAttribs["market_height"]
def UpdateMainFrameAttribs(self):
if self.IsIconized():
return
@@ -250,6 +254,9 @@ class MainFrame(wx.Frame):
self.mainFrameAttribs["wnd_height"] = height
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):
self.activeStatsWnd = wnd
@@ -423,12 +430,6 @@ class MainFrame(wx.Frame):
ctabnext = 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
self.Bind(wx.EVT_MENU, self.CloseCurrentPage, id=self.closePageId)
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.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),
(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.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_PAGEUP, ctabprev),
(wx.ACCEL_CMD, wx.WXK_PAGEDOWN, ctabnext),
(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)
(wx.ACCEL_CMD, wx.WXK_PAGEUP, ctabprev)
]
# 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)
self.SetAcceleratorTable(atable)
def AdditionsTabSelect(self, event):
selTab = None
if event.GetId() == self.additionstab1:
selTab = 0
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:
selTab = self.additionsSelect.index(event.GetId())
if selTab <= self.additionsPane.notebook.GetPageCount():
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):
self.fitMultiSwitch.NextPage()

View File

@@ -438,6 +438,11 @@ class ItemView(d.Display):
self.metalvls = sMkt.directAttrRequest(items, attrs)
# Re-sort stuff
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)
def makeReverseMetaMap(self):

View File

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

View File

@@ -939,11 +939,12 @@ class ShipBrowser(wx.Panel):
class PFStaticText(wx.Panel):
def __init__(self, parent, label=wx.EmptyString):
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)
text = wx.StaticText( self, wx.ID_ANY, label, wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_CENTRE )
text.Wrap( -1 )
mainSizer.Add( text, 1, wx.EXPAND|wx.TOP, 10 )
mainSizer.Add( text, 1, wx.ALL, 10 )
self.SetSizer(mainSizer)
self.Layout()
def GetType(self):
@@ -1426,9 +1427,6 @@ class FitItem(SFItem.SFBrowserItem):
self.deleted = False
# @todo: replace all getActiveFit() in class with this variable and test
self.activeFit = self.mainFrame.getActiveFit()
if shipID:
self.shipBmp = bitmapLoader.getBitmap(str(shipID),"ships")
@@ -1441,19 +1439,6 @@ class FitItem(SFItem.SFBrowserItem):
# see GH issue #62
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.copyBmp = bitmapLoader.getBitmap("fit_add_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)
def OnPopupItemSelected(self, event):
''' Fires when fit menu item is selected '''
# currently only have one menu option (toggle booster)
def OnToggleBooster(self, event):
sFit = service.Fit.getInstance()
sFit.toggleBoostFit(self.fitID)
self.fitBooster = not self.fitBooster
self.boosterBtn.Show(self.fitBooster)
self.fitMenu.Check(self.toggleItem.GetId(), self.fitBooster)
self.Refresh()
wx.PostEvent(self.mainFrame, BoosterListUpdated())
event.Skip()
@@ -1545,9 +1528,23 @@ class FitItem(SFItem.SFBrowserItem):
''' Handles context menu for fit. Dragging is handled by MouseLeftUp() '''
pos = wx.GetMousePosition()
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
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()

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
#======================================================================
# Copyright (C) 2012 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 3 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/>.
#======================================================================
import os
import sys
# Add eos root path to sys.path so we can import ourselves
path = os.path.dirname(unicode(__file__, sys.getfilesystemencoding()))
sys.path.append(os.path.realpath(os.path.join(path, "..")))
import json
import argparse
def main(db, json_path):
jsonPath = os.path.expanduser(json_path)
# Import eos.config first and change it
import eos.config
eos.config.gamedata_connectionstring = db
eos.config.debug = False
# Now thats done, we can import the eos modules using the config
import eos.db
import eos.gamedata
# Create the database tables
eos.db.gamedata_meta.create_all()
# Config dict
tables = {
"dgmattribs": eos.gamedata.AttributeInfo,
"dgmeffects": eos.gamedata.EffectInfo,
"dgmtypeattribs": eos.gamedata.Attribute,
"dgmtypeeffects": eos.gamedata.Effect,
"dgmunits": eos.gamedata.Unit,
"icons": eos.gamedata.Icon,
"invcategories": eos.gamedata.Category,
"invgroups": eos.gamedata.Group,
"invmetagroups": eos.gamedata.MetaGroup,
"invmetatypes": eos.gamedata.MetaType,
"invtypes": eos.gamedata.Item,
"phbtraits": eos.gamedata.Traits,
"phbmetadata": eos.gamedata.MetaData,
"mapbulk_marketGroups": eos.gamedata.MarketGroup
}
fieldMapping = {
"dgmattribs": {
"displayName_en-us": "displayName"
},
"dgmeffects": {
"displayName_en-us": "displayName",
"description_en-us": "description"
},
"dgmunits": {
"displayName_en-us": "displayName"
},
#icons???
"invcategories": {
"categoryName_en-us": "categoryName"
},
"invgroups": {
"groupName_en-us": "groupName"
},
"invmetagroups": {
"metaGroupName_en-us": "metaGroupName"
},
"invtypes": {
"typeName_en-us": "typeName",
"description_en-us": "description"
},
#phbtraits???
"mapbulk_marketGroups": {
"marketGroupName_en-us": "marketGroupName",
"description_en-us": "description"
}
}
def convertIcons(data):
new = []
for k, v in data.items():
v["iconID"] = k
new.append(v)
return new
def convertTraits(data):
def convertSection(sectionData):
sectionLines = []
headerText = u"<b>{}</b>".format(sectionData["header"])
sectionLines.append(headerText)
for bonusData in sectionData["bonuses"]:
prefix = u"{} ".format(bonusData["number"]) if "number" in bonusData else ""
bonusText = u"{}{}".format(prefix, bonusData["text"].replace(u"\u00B7", u"\u2022 "))
sectionLines.append(bonusText)
sectionLine = u"<br />\n".join(sectionLines)
return sectionLine
newData = []
for row in data:
typeLines = []
typeId = row["typeID"]
traitData = row["traits_en-us"]
for skillData in sorted(traitData.get("skills", ()), key=lambda i: i["header"]):
typeLines.append(convertSection(skillData))
if "role" in traitData:
typeLines.append(convertSection(traitData["role"]))
if "misc" in traitData:
typeLines.append(convertSection(traitData["misc"]))
traitLine = u"<br />\n<br />\n".join(typeLines)
newRow = {"typeID": typeId, "traitText": traitLine}
newData.append(newRow)
return newData
def convertTypes(typesData):
"""
Add factionID column to invtypes table.
"""
factionMap = {}
with open(os.path.join(jsonPath, "fsdTypeOverrides.json")) as f:
overridesData = json.load(f)
for typeID, typeData in overridesData.items():
factionID = typeData.get("factionID")
if factionID is not None:
factionMap[int(typeID)] = factionID
for row in typesData:
row['factionID'] = factionMap.get(int(row['typeID']))
return typesData
data = {}
# Dump all data to memory so we can easely cross check ignored rows
for jsonName, cls in tables.iteritems():
with open(os.path.join(jsonPath, "{}.json".format(jsonName))) as f:
tableData = json.load(f)
if jsonName == "icons":
tableData = convertIcons(tableData)
if jsonName == "phbtraits":
tableData = convertTraits(tableData)
if jsonName == "invtypes":
tableData = convertTypes(tableData)
data[jsonName] = tableData
# Set with typeIDs which we will have in our database
invTypes = set()
for row in data["invtypes"]:
# 1306 - group Ship Modifiers, for items like tactical t3 ship modes
if (row["published"] or row['groupID'] == 1306):
invTypes.add(row["typeID"])
# ignore checker
def isIgnored(file, row):
if file in ("invtypes", "dgmtypeeffects", "dgmtypeattribs", "invmetatypes") and row['typeID'] not in invTypes:
return True
return False
# Loop through each json file and write it away, checking ignored rows
for jsonName, table in data.iteritems():
fieldMap = fieldMapping.get(jsonName, {})
print "processing {}".format(jsonName)
for row in table:
# We don't care about some kind of rows, filter it out if so
if not isIgnored(jsonName, row):
instance = tables[jsonName]()
# fix for issue 80
if jsonName is "icons" and "res:/UI/Texture/Icons/" in str(row["iconFile"]):
row["iconFile"] = row["iconFile"].replace("res:/UI/Texture/Icons/","").replace(".png", "")
for k, v in row.iteritems():
setattr(instance, fieldMap.get(k, k), v)
eos.db.gamedata_session.add(instance)
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)
#!/usr/bin/env python
#======================================================================
# Copyright (C) 2012 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 3 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/>.
#======================================================================
import os
import sys
# Add eos root path to sys.path so we can import ourselves
path = os.path.dirname(unicode(__file__, sys.getfilesystemencoding()))
sys.path.append(os.path.realpath(os.path.join(path, "..")))
import json
import argparse
def main(db, json_path):
jsonPath = os.path.expanduser(json_path)
# Import eos.config first and change it
import eos.config
eos.config.gamedata_connectionstring = db
eos.config.debug = False
# Now thats done, we can import the eos modules using the config
import eos.db
import eos.gamedata
# Create the database tables
eos.db.gamedata_meta.create_all()
# Config dict
tables = {
"dgmattribs": eos.gamedata.AttributeInfo,
"dgmeffects": eos.gamedata.EffectInfo,
"dgmtypeattribs": eos.gamedata.Attribute,
"dgmtypeeffects": eos.gamedata.Effect,
"dgmunits": eos.gamedata.Unit,
"icons": eos.gamedata.Icon,
"evecategories": eos.gamedata.Category,
"evegroups": eos.gamedata.Group,
"invmetagroups": eos.gamedata.MetaGroup,
"invmetatypes": eos.gamedata.MetaType,
"evetypes": eos.gamedata.Item,
"phbtraits": eos.gamedata.Traits,
"phbmetadata": eos.gamedata.MetaData,
"mapbulk_marketGroups": eos.gamedata.MarketGroup
}
fieldMapping = {
"dgmattribs": {
"displayName_en-us": "displayName"
},
"dgmeffects": {
"displayName_en-us": "displayName",
"description_en-us": "description"
},
"dgmunits": {
"displayName_en-us": "displayName"
},
#icons???
"evecategories": {
"categoryName_en-us": "categoryName"
},
"evegroups": {
"groupName_en-us": "groupName"
},
"invmetagroups": {
"metaGroupName_en-us": "metaGroupName"
},
"evetypes": {
"typeName_en-us": "typeName",
"description_en-us": "description"
},
#phbtraits???
"mapbulk_marketGroups": {
"marketGroupName_en-us": "marketGroupName",
"description_en-us": "description"
}
}
rowsInValues = (
"evetypes",
"evegroups",
"evecategories"
)
def convertIcons(data):
new = []
for k, v in data.items():
v["iconID"] = k
new.append(v)
return new
def convertTraits(data):
def convertSection(sectionData):
sectionLines = []
headerText = u"<b>{}</b>".format(sectionData["header"])
sectionLines.append(headerText)
for bonusData in sectionData["bonuses"]:
prefix = u"{} ".format(bonusData["number"]) if "number" in bonusData else ""
bonusText = u"{}{}".format(prefix, bonusData["text"].replace(u"\u00B7", u"\u2022 "))
sectionLines.append(bonusText)
sectionLine = u"<br />\n".join(sectionLines)
return sectionLine
newData = []
for row in data:
typeLines = []
typeId = row["typeID"]
traitData = row["traits_en-us"]
for skillData in sorted(traitData.get("skills", ()), key=lambda i: i["header"]):
typeLines.append(convertSection(skillData))
if "role" in traitData:
typeLines.append(convertSection(traitData["role"]))
if "misc" in traitData:
typeLines.append(convertSection(traitData["misc"]))
traitLine = u"<br />\n<br />\n".join(typeLines)
newRow = {"typeID": typeId, "traitText": traitLine}
newData.append(newRow)
return newData
def convertTypes(typesData):
"""
Add factionID column to evetypes table.
"""
factionMap = {}
with open(os.path.join(jsonPath, "fsdTypeOverrides.json")) as f:
overridesData = json.load(f)
for typeID, typeData in overridesData.items():
factionID = typeData.get("factionID")
if factionID is not None:
factionMap[int(typeID)] = factionID
for row in typesData:
row['factionID'] = factionMap.get(int(row['typeID']))
return typesData
data = {}
# Dump all data to memory so we can easely cross check ignored rows
for jsonName, cls in tables.iteritems():
with open(os.path.join(jsonPath, "{}.json".format(jsonName))) as f:
tableData = json.load(f)
if jsonName in rowsInValues:
tableData = list(tableData.values())
if jsonName == "icons":
tableData = convertIcons(tableData)
if jsonName == "phbtraits":
tableData = convertTraits(tableData)
if jsonName == "evetypes":
tableData = convertTypes(tableData)
data[jsonName] = tableData
# Set with typeIDs which we will have in our database
# Sometimes CCP unpublishes some items we want to have published, we
# can do it here - just add them to initial set
eveTypes = set()
for row in data["evetypes"]:
# 1306 - group Ship Modifiers, for items like tactical t3 ship modes
if (row["published"] or row['groupID'] == 1306):
eveTypes.add(row["typeID"])
# ignore checker
def isIgnored(file, row):
if file in ("evetypes", "dgmtypeeffects", "dgmtypeattribs", "invmetatypes") and row['typeID'] not in eveTypes:
return True
return False
# Loop through each json file and write it away, checking ignored rows
for jsonName, table in data.iteritems():
fieldMap = fieldMapping.get(jsonName, {})
print "processing {}".format(jsonName)
for row in table:
# We don't care about some kind of rows, filter it out if so
if not isIgnored(jsonName, row):
instance = tables[jsonName]()
# fix for issue 80
if jsonName is "icons" and "res:/UI/Texture/Icons/" in str(row["iconFile"]):
row["iconFile"] = row["iconFile"].replace("res:/UI/Texture/Icons/","").replace(".png", "")
for k, v in row.iteritems():
setattr(instance, fieldMap.get(k, k), v)
eos.db.gamedata_session.add(instance)
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.port import Port
logger = logging.getLogger("pyfa.service.fit")
logger = logging.getLogger(__name__)
class FitBackupThread(threading.Thread):
def __init__(self, path, callback):
@@ -97,7 +97,8 @@ class Fit(object):
"rackSlots": True,
"rackLabels": True,
"compactSkills": True,
"showTooltip": True}
"showTooltip": True,
"showMarketShortcuts": False}
self.serviceFittingOptions = SettingsProvider.getInstance().getSettings(
"pyfaServiceFittingOptions", serviceFittingDefaultOptions)
@@ -149,8 +150,8 @@ class Fit(object):
return fit.modules[pos]
def newFit(self, shipID, name=None):
fit = eos.types.Fit()
fit.ship = eos.types.Ship(eos.db.getItem(shipID))
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.damagePattern = self.pattern
fit.targetResists = self.targetResists
@@ -174,10 +175,14 @@ class Fit(object):
fit = eos.db.getFit(fitID)
sFleet = Fleet.getInstance()
sFleet.removeAssociatedFleetData(fit)
self.removeProjectedData(fitID)
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):
fit = eos.db.getFit(fitID)
newFit = copy.deepcopy(fit)
@@ -192,14 +197,6 @@ class Fit(object):
fit.clear()
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):
if fitID is None:
return None
@@ -235,6 +232,7 @@ class Fit(object):
return None
fit = eos.db.getFit(fitID)
inited = getattr(fit, "inited", None)
if inited is None or inited is False:
sFleet = Fleet.getInstance()
f = sFleet.getLinearFleet(fit)
@@ -245,6 +243,7 @@ class Fit(object):
fit.fleet = f
if not projected:
print "Not projected, getting projected fits"
for fitP in fit.projectedFits:
self.getFit(fitP.ID, projected = True)
self.recalc(fit, withBoosters=True)
@@ -274,7 +273,6 @@ class Fit(object):
except ValueError:
return False
fit.implants.freeSlot(implant)
fit.implants.append(implant)
self.recalc(fit)
return True
@@ -300,7 +298,6 @@ class Fit(object):
except ValueError:
return False
fit.boosters.freeSlot(booster)
fit.boosters.append(booster)
self.recalc(fit)
return True
@@ -323,9 +320,14 @@ class Fit(object):
eager=("attributes", "group.category"))
if isinstance(thing, eos.types.Fit):
if thing.ID == fitID:
if thing in fit.projectedFits:
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":
drone = None
for d in fit.projectedDrones.find(thing):
@@ -364,6 +366,22 @@ class Fit(object):
thing.state = self.__getProposedState(thing, click)
if not thing.canHaveState(thing.state, fit):
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()
self.recalc(fit)
@@ -375,7 +393,8 @@ class Fit(object):
elif isinstance(thing, eos.types.Module):
fit.projectedModules.remove(thing)
else:
fit.projectedFits.remove(thing)
del fit.__projectedFits[thing.ID]
#fit.projectedFits.remove(thing)
eos.db.commit()
self.recalc(fit)
@@ -398,8 +417,11 @@ class Fit(object):
if m.isValidState(State.ACTIVE):
m.state = State.ACTIVE
# As some items may affect state-limiting attributes of the ship, calculate new attributes first
self.recalc(fit)
# Then, check states of all modules and change where needed. This will recalc if needed
self.checkStates(fit, m)
fit.fill()
eos.db.commit()
@@ -857,7 +879,10 @@ class Fit(object):
if drone.amountActive > 0 and not drone.canBeApplied(fit):
drone.amountActive = 0
changed = True
return changed
# If any state was changed, recalculate attributes again
if changed:
self.recalc(fit)
def toggleModulesState(self, fitID, base, modules, 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
self.recalc(fit)
# Then, check states of all modules and change where needed
changed = self.checkStates(fit, base)
# If any state was changed, recalulate attributes again
if changed is True:
self.recalc(fit)
# Then, check states of all modules and change where needed. This will recalc if needed
self.checkStates(fit, base)
# Old state : New State
localMap = {State.OVERHEATED: State.ACTIVE,
@@ -890,7 +912,7 @@ class Fit(object):
State.ONLINE: State.ACTIVE} # Just in case
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
currState = mod.state
@@ -920,6 +942,7 @@ class Fit(object):
self.recalc(fit)
def recalc(self, fit, withBoosters=False):
logger.debug("="*10+"recalc"+"="*10)
if fit.factorReload is not self.serviceFittingOptions["useGlobalForceReload"]:
fit.factorReload = self.serviceFittingOptions["useGlobalForceReload"]
fit.clear()

View File

@@ -71,6 +71,7 @@ class ShipBrowserWorkerThread(threading.Thread):
class PriceWorkerThread(threading.Thread):
def run(self):
self.queue = Queue.Queue()
self.wait = {}
self.processUpdates()
def processUpdates(self):
@@ -86,9 +87,21 @@ class PriceWorkerThread(threading.Thread):
wx.CallAfter(callback)
queue.task_done()
# After we fetch prices, go through the list of waiting items and call their callbacks
for price in requests:
callbacks = self.wait.pop(price.typeID, None)
if callbacks:
for callback in callbacks:
wx.CallAfter(callback)
def trigger(self, prices, callbacks):
self.queue.put((callbacks, prices))
def setToWait(self, itemID, callback):
if itemID not in self.wait:
self.wait[itemID] = []
self.wait[itemID].append(callback)
class SearchWorkerThread(threading.Thread):
def run(self):
self.cv = threading.Condition()
@@ -218,6 +231,8 @@ class Market():
"Mobile Decoy Unit": False, # Seems to be left over test mod for deployables
"Tournament Micro Jump Unit": False, # Normally seen only on tournament arenas
"Council Diplomatic Shuttle": False, # CSM X celebration
"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
@@ -690,10 +705,6 @@ class Market():
self.priceCache[typeID] = price
if not price.isValid:
# if the price has expired
price.price = None
return price
def getPricesNow(self, typeIDs):
@@ -716,6 +727,21 @@ class Market():
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):
self.priceCache.clear()
deleted_rows = eos.db.clearPrices()

View File

@@ -55,4 +55,4 @@ else:
# If database does not exist, do not worry about migration. Simply
# create and set version
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