Compare commits

...

44 Commits

Author SHA1 Message Date
DarkPhoenix
855fafa94d Bump version 2020-04-24 15:35:52 +03:00
DarkPhoenix
4e10335ae7 Revert "Try resetting locale on wx 4.0.6 as an attempt to work around #2174"
This reverts commit ea07bbf4f9.
2020-04-24 15:10:33 +03:00
DarkPhoenix
21ea9ce579 "Move" attributes at DB generation time 2020-04-24 15:10:00 +03:00
DarkPhoenix
ea07bbf4f9 Try resetting locale on wx 4.0.6 as an attempt to work around #2174 2020-04-24 11:37:27 +03:00
DarkPhoenix
8eed6fbe21 Ignore mutations with 0 base value 2020-04-23 19:47:57 +03:00
DarkPhoenix
0859f2fbe9 Hide limited synth boosters 2020-04-22 02:27:20 +03:00
DarkPhoenix
71ba33edeb Fix tail clear method 2020-04-21 19:00:44 +03:00
DarkPhoenix
ce80d92b35 Limit amount of fits returned by search by 100 2020-04-21 11:16:06 +03:00
DarkPhoenix
f17cf9b736 Fix undo for fill with module (which was caused by module add command not being undone properly in case of failure) 2020-04-20 16:36:16 +03:00
DarkPhoenix
98579c774b Change shortcuts for undo/redo from built-in wx standards to ctrl-z and ctrl-y 2020-04-20 14:42:32 +03:00
DarkPhoenix
509fa279e7 Initialize session container with main thread session 2020-04-17 19:15:47 +03:00
DarkPhoenix
091ee87761 Update char list when new character gets added 2020-04-17 13:31:15 +03:00
DarkPhoenix
c0c20cc92e Remove skill refresh button 2020-04-17 13:27:51 +03:00
DarkPhoenix
1341f7bca1 Make some jargon replacements a bit looser 2020-04-17 11:27:28 +03:00
DarkPhoenix
fe93db1d4b Round booster side-effect context menu 2020-04-17 00:25:33 +03:00
DarkPhoenix
5db97ea773 Bump version 2020-04-16 22:46:45 +03:00
DarkPhoenix
1758e4f320 Rework fix for projection, so that it does not recalculate projected fits too early 2020-04-16 22:25:10 +03:00
DarkPhoenix
1a897c0419 Pass search results as set of item IDs 2020-04-16 15:13:32 +03:00
DarkPhoenix
32db3e3179 Create scoped gamedata sessions 2020-04-16 15:01:00 +03:00
DarkPhoenix
d830a8957a Bump version 2020-04-15 16:54:24 +03:00
DarkPhoenix
652ea48223 Accept 0 free slots for guns which have already been fitted 2020-04-15 16:53:03 +03:00
DarkPhoenix
8c25b2b8f5 Make sure it's impossible to add extra cap boosters via dragging too 2020-04-15 15:40:54 +03:00
DarkPhoenix
db4c56be8e Bump version 2020-04-15 15:27:19 +03:00
DarkPhoenix
f3bcffe2f9 Implement fax cap booster limit 2020-04-15 15:23:49 +03:00
DarkPhoenix
bc5786d099 Update static data to 1706308 2020-04-15 14:50:55 +03:00
DarkPhoenix
5959fe5daf Ignore case when searching for implant sets to allow some inconsistencies on CCP side 2020-04-14 15:28:40 +03:00
DarkPhoenix
649d338bb1 Split implants and boosters in EFT and multibuy exports 2020-04-14 11:36:08 +03:00
DarkPhoenix
dcb058a718 Update existing character if character with the same name exists 2020-04-13 12:52:02 +03:00
DarkPhoenix
1772bb5e7f Merge branch 'master' into singularity 2020-04-13 12:45:25 +03:00
Anton Vorobyov
30bd0adb06 Merge pull request #2159 from soro/auto_create_char
Regarding #2045: automatically create a character for a new sso character
2020-04-12 22:49:01 +03:00
DarkPhoenix
44dfcf771c Ensure projected fits get recalculated, since they get checkStates'd anyway 2020-04-11 02:26:17 +03:00
DarkPhoenix
a1f8a7a930 Fix escaping in regex search 2020-04-11 01:33:53 +03:00
Soeren Roerden
b22887dfad automatically create a character for a new sso character and add button to retrieve all chars directly linked to existing sso characters 2020-04-10 21:12:38 +02:00
DarkPhoenix
28137fa3f4 Remove excessive space 2020-04-10 13:11:04 +03:00
DarkPhoenix
9cbdc6055d Add search timer to attribute overrides to prevent hangs 2020-04-10 11:52:57 +03:00
DarkPhoenix
fc93c61fcf Add more entries 2020-04-10 11:31:17 +03:00
DarkPhoenix
3fa2e7ebd1 More additions to jargon 2020-04-10 06:07:33 +03:00
DarkPhoenix
818628da0c Finalize new jargon dictionary 2020-04-10 05:53:24 +03:00
DarkPhoenix
adf90a8263 Split basic and regex search functions 2020-04-10 00:57:41 +03:00
DarkPhoenix
362923ac64 Overhaul jargon dictionary (some entries are still missing) 2020-04-09 23:29:51 +03:00
DarkPhoenix
7d73838ce1 Change the way user definitions are used 2020-04-08 14:06:49 +03:00
DarkPhoenix
b3278ca9ec Tokenize regexp requests taking into consideration regexp context 2020-04-08 13:24:48 +03:00
DarkPhoenix
5707914ad5 Make use of regex for search 2020-04-08 03:17:25 +03:00
DarkPhoenix
9b697b24d8 Implement regexp function for gamedata 2020-04-08 02:34:28 +03:00
44 changed files with 3475 additions and 1204 deletions

View File

@@ -125,6 +125,8 @@ def update_db():
row['typeName'] in ('Capsule', 'Dark Blood Tracking Disruptor')
):
row['published'] = True
elif row['typeName'].startswith('Limited Synth '):
row['published'] = False
newData = []
for row in data:
@@ -168,16 +170,26 @@ def update_db():
data = _readData('fsd_binary', 'typedogma', keyIdName='typeID')
eveTypeIds = set(r['typeID'] for r in eveTypesData)
newData = []
for row in eveTypesData:
for attrId, attrName in {4: 'mass', 38: 'capacity', 161: 'volume', 162: 'radius'}.items():
if attrName in row:
newData.append({'typeID': row['typeID'], 'attributeID': attrId, 'value': row[attrName]})
seenKeys = set()
def checkKey(key):
if key in seenKeys:
return False
seenKeys.add(key)
return True
for typeData in data:
if typeData['typeID'] not in eveTypeIds:
continue
for row in typeData.get('dogmaAttributes', ()):
row['typeID'] = typeData['typeID']
newData.append(row)
if checkKey((row['typeID'], row['attributeID'])):
newData.append(row)
for row in eveTypesData:
for attrId, attrName in {4: 'mass', 38: 'capacity', 161: 'volume', 162: 'radius'}.items():
if attrName in row and checkKey((row['typeID'], attrId)):
newData.append({'typeID': row['typeID'], 'attributeID': attrId, 'value': row[attrName]})
_addRows(newData, eos.gamedata.Attribute)
return newData
@@ -472,7 +484,7 @@ def update_db():
continue
typeName = row.get('typeName', '')
# Regular sets matching
m = re.match('(?P<grade>(High|Mid|Low)-grade) (?P<set>\w+) (?P<implant>(Alpha|Beta|Gamma|Delta|Epsilon|Omega))', typeName)
m = re.match('(?P<grade>(High|Mid|Low)-grade) (?P<set>\w+) (?P<implant>(Alpha|Beta|Gamma|Delta|Epsilon|Omega))', typeName, re.IGNORECASE)
if m:
implantSets.setdefault((m.group('grade'), m.group('set')), set()).add(row['typeID'])
# Special set matching

View File

@@ -17,24 +17,37 @@
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import re
import threading
from sqlalchemy import MetaData, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy import MetaData, create_engine, event
from sqlalchemy.orm import sessionmaker, scoped_session
from . import migration
from eos import config
from logbook import Logger
pyfalog = Logger(__name__)
pyfalog.info("Initializing database")
pyfalog.info("Gamedata connection: {0}", config.gamedata_connectionstring)
pyfalog.info("Saveddata connection: {0}", config.saveddata_connectionstring)
class ReadOnlyException(Exception):
pass
def re_fn(expr, item):
try:
reg = re.compile(expr, re.IGNORECASE)
except (SystemExit, KeyboardInterrupt):
raise
except:
return False
return reg.search(item) is not None
pyfalog.debug('Initializing gamedata')
gamedata_connectionstring = config.gamedata_connectionstring
if callable(gamedata_connectionstring):
@@ -42,9 +55,26 @@ if callable(gamedata_connectionstring):
else:
gamedata_engine = create_engine(gamedata_connectionstring, echo=config.debug)
@event.listens_for(gamedata_engine, 'connect')
def create_functions(dbapi_connection, connection_record):
dbapi_connection.create_function('regexp', 2, re_fn)
gamedata_meta = MetaData()
gamedata_meta.bind = gamedata_engine
gamedata_session = sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False)()
GamedataSession = scoped_session(sessionmaker(bind=gamedata_engine, autoflush=False, expire_on_commit=False))
gamedata_session = GamedataSession()
gamedata_sessions = {threading.get_ident(): gamedata_session}
def get_gamedata_session():
thread_id = threading.get_ident()
if thread_id not in gamedata_sessions:
gamedata_sessions[thread_id] = GamedataSession()
return gamedata_sessions[thread_id]
pyfalog.debug('Getting gamedata version')
# This should be moved elsewhere, maybe as an actual query. Current, without try-except, it breaks when making a new

View File

@@ -33,9 +33,6 @@ items_table = Table("invtypes", gamedata_meta,
Column("description", String),
Column("raceID", Integer),
Column("factionID", Integer),
Column("volume", Float),
Column("mass", Float),
Column("capacity", Float),
Column("published", Boolean),
Column("marketGroupID", Integer, ForeignKey("invmarketgroups.marketGroupID")),
Column("iconID", Integer),

View File

@@ -22,7 +22,7 @@ from sqlalchemy.orm import aliased, exc, join
from sqlalchemy.sql import and_, or_, select
import eos.config
from eos.db import gamedata_session
from eos.db import get_gamedata_session
from eos.db.gamedata.item import items_table
from eos.db.gamedata.group import groups_table
from eos.db.util import processEager, processWhere
@@ -64,7 +64,7 @@ else:
return deco
def sqlizeString(line):
def sqlizeNormalString(line):
# Escape backslashes first, as they will be as escape symbol in queries
# Then escape percent and underscore signs
# Finally, replace generic wildcards with sql-style wildcards
@@ -79,29 +79,39 @@ itemNameMap = {}
def getItem(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
item = gamedata_session.query(Item).get(lookfor)
item = get_gamedata_session().query(Item).get(lookfor)
else:
item = gamedata_session.query(Item).options(*processEager(eager)).filter(Item.ID == lookfor).first()
item = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.ID == lookfor).first()
elif isinstance(lookfor, str):
if lookfor in itemNameMap:
id = itemNameMap[lookfor]
if eager is None:
item = gamedata_session.query(Item).get(id)
item = get_gamedata_session().query(Item).get(id)
else:
item = gamedata_session.query(Item).options(*processEager(eager)).filter(Item.ID == id).first()
item = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.ID == id).first()
else:
# Item names are unique, so we can use first() instead of one()
item = gamedata_session.query(Item).options(*processEager(eager)).filter(Item.name == lookfor).first()
item = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.name == lookfor).first()
if item is not None:
itemNameMap[lookfor] = item.ID
else:
raise TypeError("Need integer or string as argument")
return item
@cachedQuery(1, "itemIDs")
def getItems(itemIDs, eager=None):
if not isinstance(itemIDs, (tuple, list, set)) or not all(isinstance(t, int) for t in itemIDs):
raise TypeError("Need iterable of integers as argument")
if eager is None:
items = get_gamedata_session().query(Item).filter(Item.ID.in_(itemIDs)).all()
else:
items = get_gamedata_session().query(Item).options(*processEager(eager)).filter(Item.ID.in_(itemIDs)).all()
return items
def getMutaplasmid(lookfor, eager=None):
if isinstance(lookfor, int):
item = gamedata_session.query(DynamicItem).filter(DynamicItem.ID == lookfor).first()
item = get_gamedata_session().query(DynamicItem).filter(DynamicItem.ID == lookfor).first()
else:
raise TypeError("Need integer as argument")
return item
@@ -109,7 +119,7 @@ def getMutaplasmid(lookfor, eager=None):
def getItemWithBaseItemAttribute(lookfor, baseItemID, eager=None):
# A lot of this is described in more detail in #1597
item = gamedata_session.query(Item).get(lookfor)
item = get_gamedata_session().query(Item).get(lookfor)
base = getItem(baseItemID)
# we have to load all attributes for this object, otherwise we'll lose access to them when we expunge.
@@ -125,7 +135,7 @@ def getItemWithBaseItemAttribute(lookfor, baseItemID, eager=None):
# Expunge the item form the session. This is required to have different Abyssal / Base combinations loaded in memory.
# Without expunging it, once one Abyssal Web is created, SQLAlchmey will use it for all others. We don't want this,
# we want to generate a completely new object to work with
gamedata_session.expunge(item)
get_gamedata_session().expunge(item)
return item
@@ -148,7 +158,7 @@ def getItems(lookfor, eager=None):
if len(toGet) > 0:
# Get items that aren't currently cached, and store them in the cache
items = gamedata_session.query(Item).filter(Item.ID.in_(toGet)).all()
items = get_gamedata_session().query(Item).filter(Item.ID.in_(toGet)).all()
for item in items:
cache[(item.ID, None)] = item
results += items
@@ -162,9 +172,9 @@ def getItems(lookfor, eager=None):
def getAlphaClone(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
item = gamedata_session.query(AlphaClone).get(lookfor)
item = get_gamedata_session().query(AlphaClone).get(lookfor)
else:
item = gamedata_session.query(AlphaClone).options(*processEager(eager)).filter(AlphaClone.ID == lookfor).first()
item = get_gamedata_session().query(AlphaClone).options(*processEager(eager)).filter(AlphaClone.ID == lookfor).first()
else:
raise TypeError("Need integer as argument")
return item
@@ -172,7 +182,7 @@ def getAlphaClone(lookfor, eager=None):
def getAlphaCloneList(eager=None):
eager = processEager(eager)
clones = gamedata_session.query(AlphaClone).options(*eager).all()
clones = get_gamedata_session().query(AlphaClone).options(*eager).all()
return clones
@@ -183,19 +193,19 @@ groupNameMap = {}
def getGroup(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
group = gamedata_session.query(Group).get(lookfor)
group = get_gamedata_session().query(Group).get(lookfor)
else:
group = gamedata_session.query(Group).options(*processEager(eager)).filter(Group.ID == lookfor).first()
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.ID == lookfor).first()
elif isinstance(lookfor, str):
if lookfor in groupNameMap:
id = groupNameMap[lookfor]
if eager is None:
group = gamedata_session.query(Group).get(id)
group = get_gamedata_session().query(Group).get(id)
else:
group = gamedata_session.query(Group).options(*processEager(eager)).filter(Group.ID == id).first()
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.ID == id).first()
else:
# Group names are unique, so we can use first() instead of one()
group = gamedata_session.query(Group).options(*processEager(eager)).filter(Group.name == lookfor).first()
group = get_gamedata_session().query(Group).options(*processEager(eager)).filter(Group.name == lookfor).first()
if group is not None:
groupNameMap[lookfor] = group.ID
else:
@@ -210,21 +220,21 @@ categoryNameMap = {}
def getCategory(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
category = gamedata_session.query(Category).get(lookfor)
category = get_gamedata_session().query(Category).get(lookfor)
else:
category = gamedata_session.query(Category).options(*processEager(eager)).filter(
category = get_gamedata_session().query(Category).options(*processEager(eager)).filter(
Category.ID == lookfor).first()
elif isinstance(lookfor, str):
if lookfor in categoryNameMap:
id = categoryNameMap[lookfor]
if eager is None:
category = gamedata_session.query(Category).get(id)
category = get_gamedata_session().query(Category).get(id)
else:
category = gamedata_session.query(Category).options(*processEager(eager)).filter(
category = get_gamedata_session().query(Category).options(*processEager(eager)).filter(
Category.ID == id).first()
else:
# Category names are unique, so we can use first() instead of one()
category = gamedata_session.query(Category).options(*processEager(eager)).filter(
category = get_gamedata_session().query(Category).options(*processEager(eager)).filter(
Category.name == lookfor).first()
if category is not None:
categoryNameMap[lookfor] = category.ID
@@ -240,21 +250,21 @@ metaGroupNameMap = {}
def getMetaGroup(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
metaGroup = gamedata_session.query(MetaGroup).get(lookfor)
metaGroup = get_gamedata_session().query(MetaGroup).get(lookfor)
else:
metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter(
metaGroup = get_gamedata_session().query(MetaGroup).options(*processEager(eager)).filter(
MetaGroup.ID == lookfor).first()
elif isinstance(lookfor, str):
if lookfor in metaGroupNameMap:
id = metaGroupNameMap[lookfor]
if eager is None:
metaGroup = gamedata_session.query(MetaGroup).get(id)
metaGroup = get_gamedata_session().query(MetaGroup).get(id)
else:
metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter(
metaGroup = get_gamedata_session().query(MetaGroup).options(*processEager(eager)).filter(
MetaGroup.ID == id).first()
else:
# MetaGroup names are unique, so we can use first() instead of one()
metaGroup = gamedata_session.query(MetaGroup).options(*processEager(eager)).filter(
metaGroup = get_gamedata_session().query(MetaGroup).options(*processEager(eager)).filter(
MetaGroup.name == lookfor).first()
if metaGroup is not None:
metaGroupNameMap[lookfor] = metaGroup.ID
@@ -264,16 +274,16 @@ def getMetaGroup(lookfor, eager=None):
def getMetaGroups():
return gamedata_session.query(MetaGroup).all()
return get_gamedata_session().query(MetaGroup).all()
@cachedQuery(1, "lookfor")
def getMarketGroup(lookfor, eager=None):
if isinstance(lookfor, int):
if eager is None:
marketGroup = gamedata_session.query(MarketGroup).get(lookfor)
marketGroup = get_gamedata_session().query(MarketGroup).get(lookfor)
else:
marketGroup = gamedata_session.query(MarketGroup).options(*processEager(eager)).filter(
marketGroup = get_gamedata_session().query(MarketGroup).options(*processEager(eager)).filter(
MarketGroup.ID == lookfor).first()
else:
raise TypeError("Need integer as argument")
@@ -285,7 +295,7 @@ def getMarketTreeNodeIds(rootNodeIds):
addedIds = set(rootNodeIds)
while addedIds:
allIds.update(addedIds)
addedIds = {mg.ID for mg in gamedata_session.query(MarketGroup).filter(MarketGroup.parentGroupID.in_(addedIds))}
addedIds = {mg.ID for mg in get_gamedata_session().query(MarketGroup).filter(MarketGroup.parentGroupID.in_(addedIds))}
return allIds
@@ -299,7 +309,7 @@ def getItemsByCategory(filter, where=None, eager=None):
raise TypeError("Need integer or string as argument")
filter = processWhere(filter, where)
return gamedata_session.query(Item).options(*processEager(eager)).join(Item.group, Group.category).filter(
return get_gamedata_session().query(Item).options(*processEager(eager)).join(Item.group, Group.category).filter(
filter).all()
@@ -314,9 +324,9 @@ def searchItems(nameLike, where=None, join=None, eager=None):
if not hasattr(join, "__iter__"):
join = (join,)
items = gamedata_session.query(Item).options(*processEager(eager)).join(*join)
items = get_gamedata_session().query(Item).options(*processEager(eager)).join(*join)
for token in nameLike.split(' '):
token_safe = "%{0}%".format(sqlizeString(token))
token_safe = "%{0}%".format(sqlizeNormalString(token))
if where is not None:
items = items.filter(and_(Item.name.like(token_safe, escape="\\"), where))
else:
@@ -325,14 +335,35 @@ def searchItems(nameLike, where=None, join=None, eager=None):
return items
@cachedQuery(3, "tokens", "where", "join")
def searchItemsRegex(tokens, where=None, join=None, eager=None):
if not isinstance(tokens, (tuple, list)) or not all(isinstance(t, str) for t in tokens):
raise TypeError("Need tuple or list of strings as argument")
if join is None:
join = tuple()
if not hasattr(join, "__iter__"):
join = (join,)
items = get_gamedata_session().query(Item).options(*processEager(eager)).join(*join)
for token in tokens:
if where is not None:
items = items.filter(and_(Item.name.op('regexp')(token), where))
else:
items = items.filter(Item.name.op('regexp')(token))
items = items.limit(100).all()
return items
@cachedQuery(3, "where", "nameLike", "join")
def searchSkills(nameLike, where=None, eager=None):
if not isinstance(nameLike, str):
raise TypeError("Need string as argument")
items = gamedata_session.query(Item).options(*processEager(eager)).join(Item.group, Group.category)
items = get_gamedata_session().query(Item).options(*processEager(eager)).join(Item.group, Group.category)
for token in nameLike.split(' '):
token_safe = "%{0}%".format(sqlizeString(token))
token_safe = "%{0}%".format(sqlizeNormalString(token))
if where is not None:
items = items.filter(and_(Item.name.like(token_safe, escape="\\"), Category.ID == 16, where))
else:
@@ -352,7 +383,7 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None):
itemfilter = or_(*(items_table.c.variationParentTypeID == itemid for itemid in itemids))
filter = processWhere(itemfilter, where)
vars = gamedata_session.query(Item).options(*processEager(eager)).filter(filter).all()
vars = get_gamedata_session().query(Item).options(*processEager(eager)).filter(filter).all()
if vars:
return vars
@@ -360,7 +391,7 @@ def getVariations(itemids, groupIDs=None, where=None, eager=None):
itemfilter = or_(*(groups_table.c.groupID == groupID for groupID in groupIDs))
filter = processWhere(itemfilter, where)
joinon = items_table.c.groupID == groups_table.c.groupID
vars = gamedata_session.query(Item).options(*processEager(eager)).join((groups_table, joinon)).filter(
vars = get_gamedata_session().query(Item).options(*processEager(eager)).join((groups_table, joinon)).filter(
filter).all()
return vars
@@ -375,7 +406,7 @@ def getAttributeInfo(attr, eager=None):
else:
raise TypeError("Need integer or string as argument")
try:
result = gamedata_session.query(AttributeInfo).options(*processEager(eager)).filter(filter).one()
result = get_gamedata_session().query(AttributeInfo).options(*processEager(eager)).filter(filter).one()
except exc.NoResultFound:
result = None
return result
@@ -384,7 +415,7 @@ def getAttributeInfo(attr, eager=None):
@cachedQuery(1, "field")
def getMetaData(field):
if isinstance(field, str):
data = gamedata_session.query(MetaData).get(field)
data = get_gamedata_session().query(MetaData).get(field)
else:
raise TypeError("Need string as argument")
return data
@@ -403,12 +434,12 @@ def directAttributeRequest(itemIDs, attrIDs):
and_(Attribute.attributeID.in_(attrIDs), Item.typeID.in_(itemIDs)),
from_obj=[join(Attribute, Item)])
result = gamedata_session.execute(q).fetchall()
result = get_gamedata_session().execute(q).fetchall()
return result
def getAbyssalTypes():
return set([r.resultingTypeID for r in gamedata_session.query(DynamicItem.resultingTypeID).distinct()])
return set([r.resultingTypeID for r in get_gamedata_session().query(DynamicItem.resultingTypeID).distinct()])
@cachedQuery(1, "itemID")
@@ -416,9 +447,9 @@ def getDynamicItem(itemID, eager=None):
try:
if isinstance(itemID, int):
if eager is None:
result = gamedata_session.query(DynamicItem).filter(DynamicItem.ID == itemID).one()
result = get_gamedata_session().query(DynamicItem).filter(DynamicItem.ID == itemID).one()
else:
result = gamedata_session.query(DynamicItem).options(*processEager(eager)).filter(DynamicItem.ID == itemID).one()
result = get_gamedata_session().query(DynamicItem).options(*processEager(eager)).filter(DynamicItem.ID == itemID).one()
else:
raise TypeError("Need integer as argument")
except exc.NoResultFound:
@@ -428,5 +459,5 @@ def getDynamicItem(itemID, eager=None):
@cachedQuery(1, "lookfor")
def getAllImplantSets():
implantSets = gamedata_session.query(ImplantSet).all()
implantSets = get_gamedata_session().query(ImplantSet).all()
return implantSets

View File

@@ -470,7 +470,7 @@ def searchFits(nameLike, where=None, eager=None):
filter = processWhere(Fit.name.like(nameLike, escape="\\"), where)
eager = processEager(eager)
with sd_lock:
fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).all())
fits = removeInvalid(saveddata_session.query(Fit).options(*eager).filter(filter).limit(100).all())
return fits

View File

@@ -36395,3 +36395,21 @@ class Effect8026(BaseEffect):
fit.modules.filteredChargeBoost(
lambda mod: mod.charge.requiresSkill('Missile Launcher Operation'),
'aoeVelocity', implant.getModifiedItemAttr('hydraMissileExplosionVelocityBonus'), **kwargs)
class Effect8029(BaseEffect):
"""
roleBonus7CapBoosterGroupRestriction
Used by:
Ships from group: Force Auxiliary (6 of 6)
"""
type = 'passive'
@staticmethod
def handler(fit, ship, context, projectionRange, **kwargs):
for attr in ('maxGroupOnline', 'maxGroupFitted'):
fit.modules.filteredItemForce(
lambda mod: mod.item.group.name == 'Capacitor Booster',
attr, ship.getModifiedItemAttr('shipBonusRole7'), **kwargs)

View File

@@ -209,40 +209,13 @@ class Effect(EqBase):
class Item(EqBase):
MOVE_ATTRS = (4, # Mass
38, # Capacity
161) # Volume
MOVE_ATTR_INFO = None
ABYSSAL_TYPES = None
@classmethod
def getMoveAttrInfo(cls):
info = getattr(cls, "MOVE_ATTR_INFO", None)
if info is None:
cls.MOVE_ATTR_INFO = info = []
for id in cls.MOVE_ATTRS:
info.append(eos.db.getAttributeInfo(id))
return info
def moveAttrs(self):
self.__moved = True
for info in self.getMoveAttrInfo():
val = getattr(self, info.name, 0)
if val != 0:
attr = Attribute()
attr.info = info
attr.value = val
self.__attributes[info.name] = attr
@reconstructor
def init(self):
self.__race = None
self.__requiredSkills = None
self.__requiredFor = None
self.__moved = False
self.__offensive = None
self.__assistive = None
self.__overrides = None
@@ -264,9 +237,6 @@ class Item(EqBase):
@property
def attributes(self):
if not self.__moved:
self.moveAttrs()
return self.__attributes
@property

View File

@@ -21,6 +21,9 @@ from logbook import Logger
from sqlalchemy.orm import reconstructor
from eos.utils.round import roundToPrec
pyfalog = Logger(__name__)
@@ -56,9 +59,8 @@ class BoosterSideEffect:
@property
def name(self):
return "{0}% {1}".format(
self.booster.getModifiedItemAttr(self.attr),
self.__effect.getattr('displayName') or self.__effect.name,
)
roundToPrec(self.booster.getModifiedItemAttr(self.attr), 5),
self.__effect.getattr('displayName') or self.__effect.name)
@property
def attr(self):

View File

@@ -1026,6 +1026,16 @@ class Fit:
if mod.isEmpty:
del self.modules[i]
def clearTail(self):
tailPositions = {}
for mod in self.modules:
if not mod.isEmpty:
break
tailPositions[self.modules.index(mod)] = mod.slot
for pos in sorted(tailPositions, reverse=True):
self.modules.remove(self.modules[pos])
return tailPositions
@property
def modCount(self):
x = 0
@@ -1135,7 +1145,7 @@ class Fit:
def droneBayUsed(self):
amount = 0
for d in self.drones:
amount += d.item.volume * d.amount
amount += d.item.attributes['volume'].value * d.amount
return amount
@@ -1143,7 +1153,7 @@ class Fit:
def fighterBayUsed(self):
amount = 0
for f in self.fighters:
amount += f.item.volume * f.amount
amount += f.item.attributes['volume'].value * f.amount
return amount

View File

@@ -214,8 +214,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
if charge is None:
charges = 0
else:
chargeVolume = charge.volume
containerCapacity = self.item.capacity
chargeVolume = charge.attributes['volume'].value
containerCapacity = self.item.attributes['capacity'].value
if chargeVolume is None or containerCapacity is None:
charges = 0
else:
@@ -696,7 +696,7 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# Check this only if we're told to do so
if hardpointLimit:
if fit.getHardpointsFree(self.hardpoint) < 1:
if fit.getHardpointsFree(self.hardpoint) < (1 if self.owner != fit else 0):
return False
return True
@@ -778,8 +778,8 @@ class Module(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
# Check sizes, if 'charge size > module volume' it won't fit
if charge is None:
return True
chargeVolume = charge.volume
moduleCapacity = self.item.capacity
chargeVolume = charge.attributes['volume'].value
moduleCapacity = self.item.attributes['capacity'].value
if chargeVolume is not None and moduleCapacity is not None and chargeVolume > moduleCapacity:
return False

View File

@@ -81,6 +81,8 @@ class Mutator(EqBase):
@validates("value")
def validator(self, key, val):
""" Validates values as properly falling within the range of the modules' Mutaplasmid """
if self.baseValue == 0:
return 0
mod = val / self.baseValue
if self.minMod <= mod <= self.maxMod:

27
eos/utils/round.py Normal file
View File

@@ -0,0 +1,27 @@
import math
def roundToPrec(val, prec, nsValue=None):
"""
nsValue: custom value which should be used to determine normalization shift
"""
# We're not rounding integers anyway
# Also make sure that we do not ask to calculate logarithm of zero
if int(val) == val:
return int(val)
roundFactor = int(prec - math.floor(math.log10(abs(val if nsValue is None else nsValue))) - 1)
# But we don't want to round integers
if roundFactor < 0:
roundFactor = 0
# Do actual rounding
val = round(val, roundFactor)
# Make sure numbers with .0 part designating float don't get through
if int(val) == val:
val = int(val)
return val
def roundDec(val, prec):
if int(val) == val:
return int(val)
return round(val, prec)

View File

@@ -93,6 +93,8 @@ class ItemMutatorList(wx.ScrolledWindow):
first = True
for m in sorted(mod.mutators.values(), key=lambda x: x.attribute.displayName):
if m.baseValue == 0:
continue
if not first:
sizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.ALL | wx.EXPAND, 5)
first = False

View File

@@ -1,15 +1,16 @@
import wx
from logbook import Logger
from eos.saveddata.module import Module
import gui.builtinMarketBrowser.pfSearchBox as SBox
from config import slotColourMap
from eos.saveddata.module import Module
from gui.builtinMarketBrowser.events import ItemSelected, RECENTLY_USED_MODULES
from gui.contextMenu import ContextMenu
from gui.display import Display
from gui.utils.staticHelpers import DragDropHelper
from service.attribute import Attribute
from service.fit import Fit
from config import slotColourMap
from service.market import Market
pyfalog = Logger(__name__)
@@ -170,8 +171,8 @@ class ItemView(Display):
def scheduleSearch(self, event=None):
self.searchTimer.Stop() # Cancel any pending timers
search = self.marketBrowser.search.GetLineText(0)
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Make sure we do not count wildcards as search symbol
realsearch = search.replace('*', '').replace('?', '')
# Re-select market group if search query has zero length
if len(realsearch) == 0:
self.selectionMade('search')
@@ -193,10 +194,11 @@ class ItemView(Display):
self.setToggles()
self.filterItemStore()
def populateSearch(self, items):
def populateSearch(self, itemIDs):
# If we're no longer searching, dump the results
if self.marketBrowser.mode != 'search':
return
items = Market.getItems(itemIDs)
self.updateItemStore(items)
self.setToggles()
self.filterItemStore()

View File

@@ -86,8 +86,8 @@ class NavigationPanel(SFItem.SFBrowserItem):
def OnScheduleSearch(self, event):
search = self.BrowserSearchBox.GetValue()
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Make sure we do not count wildcards as search symbol
realsearch = search.replace('*', '').replace('?', '')
minChars = 1 if isStringCjk(realsearch) else 3
if len(realsearch) >= minChars:
self.lastSearch = search

View File

@@ -302,8 +302,8 @@ class ItemView(d.Display):
sMkt = Market.getInstance()
search = self.searchBox.GetLineText(0)
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Make sure we do not count wildcards as search symbol
realsearch = search.replace('*', '').replace('?', '')
# Show nothing if query is too short
if len(realsearch) < 3:
self.clearSearch()
@@ -311,12 +311,12 @@ class ItemView(d.Display):
sMkt.searchItems(search, self.populateSearch, 'implants')
def populateSearch(self, items):
def populateSearch(self, itemIDs):
if not self.IsShown():
self.parent.availableImplantsTree.Hide()
self.Show()
self.parent.Layout()
items = Market.getItems(itemIDs)
items = [i for i in items if i.group.name != 'Booster']
self.items = sorted(list(items), key=lambda i: i.name)

View File

@@ -833,7 +833,7 @@ class APIView(wx.Panel):
def fetchSkills(self, evt):
sChar = Character.getInstance()
char = self.charEditor.entityEditor.getActiveEntity()
sChar.apiFetch(char.ID, self.__fetchCallback)
sChar.apiFetch(char.ID, APIView.fetchCallback)
def addCharacter(self, event):
sEsi = Esi.getInstance()
@@ -899,7 +899,8 @@ class APIView(wx.Panel):
if event is not None:
event.Skip()
def __fetchCallback(self, e=None):
@staticmethod
def fetchCallback(e=None):
if e:
pyfalog.warn("Error fetching skill information for character for __fetchCallback")
exc_type, exc_value, exc_trace = e

View File

@@ -5,11 +5,14 @@ import requests
import wx
from logbook import Logger
import config
import gui.globalEvents as GE
from eos.db import getItem
from eos.saveddata.cargo import Cargo
from gui.auxFrame import AuxiliaryFrame
from gui.display import Display
from gui.characterEditor import APIView
from service.character import Character
from service.esi import Esi
from service.esiAccess import APIException
from service.fit import Fit
@@ -335,7 +338,7 @@ class SsoCharacterMgmt(AuxiliaryFrame):
self.addBtn = wx.Button(self, wx.ID_ANY, "Add Character", wx.DefaultPosition, wx.DefaultSize, 0)
btnSizer.Add(self.addBtn, 0, wx.ALL | wx.EXPAND, 5)
self.deleteBtn = wx.Button(self, wx.ID_ANY, "Revoke Character", wx.DefaultPosition, wx.DefaultSize, 0)
self.deleteBtn = wx.Button(self, wx.ID_ANY, "Remove Character", wx.DefaultPosition, wx.DefaultSize, 0)
btnSizer.Add(self.deleteBtn, 0, wx.ALL | wx.EXPAND, 5)
mainSizer.Add(btnSizer, 0, wx.EXPAND, 5)
@@ -355,6 +358,16 @@ class SsoCharacterMgmt(AuxiliaryFrame):
def ssoLogin(self, event):
self.popCharList()
sChar = Character.getInstance()
# Update existing pyfa character, if it doesn't exist - create new
char = sChar.getCharacter(event.character.characterName)
newChar = False
if char is None:
char = sChar.new(event.character.characterName)
newChar = True
char.setSsoCharacter(event.character, config.getClientSecret())
sChar.apiFetch(char.ID, APIView.fetchCallback)
wx.PostEvent(self.mainFrame, GE.CharListUpdated())
event.Skip()
def kbEvent(self, event):

View File

@@ -40,9 +40,6 @@ class CalcAddLocalModuleCommand(wx.Command):
position=fit.modules.index(oldMod),
newModInfo=self.newModInfo)
return self.subsystemCmd.Do()
if not newMod.fits(fit):
pyfalog.warning('Module does not fit')
return False
fit.modules.append(newMod)
if newMod not in fit.modules:
pyfalog.warning('Failed to append to list')
@@ -52,6 +49,13 @@ class CalcAddLocalModuleCommand(wx.Command):
# relationship via .owner attribute, which is handled by SQLAlchemy
eos.db.flush()
sFit.recalc(fit)
# fits() sometimes relies on recalculated on-item attributes, such as fax cap
# booster limitation, so we have to check it after recalculating and remove the
# module if the check has failed
if not newMod.fits(fit):
pyfalog.warning('Module does not fit')
self.Undo()
return False
self.savedStateCheckChanges = sFit.checkStates(fit, newMod)
return True
@@ -63,7 +67,7 @@ class CalcAddLocalModuleCommand(wx.Command):
if self.savedPosition is None:
return False
from .localRemove import CalcRemoveLocalModulesCommand
cmd = CalcRemoveLocalModulesCommand(fitID=self.fitID, positions=[self.savedPosition], recalc=False)
cmd = CalcRemoveLocalModulesCommand(fitID=self.fitID, positions=[self.savedPosition], recalc=False, clearTail=True)
if not cmd.Do():
return False
restoreCheckedStates(Fit.getInstance().getFit(self.fitID), self.savedStateCheckChanges)

View File

@@ -3,7 +3,7 @@ from logbook import Logger
import eos.db
from eos.const import FittingSlot
from gui.fitCommands.helpers import ModuleInfo, restoreCheckedStates
from gui.fitCommands.helpers import ModuleInfo, restoreCheckedStates, restoreRemovedDummies
from service.fit import Fit
@@ -12,14 +12,16 @@ pyfalog = Logger(__name__)
class CalcRemoveLocalModulesCommand(wx.Command):
def __init__(self, fitID, positions, recalc=True):
def __init__(self, fitID, positions, recalc=True, clearTail=False):
wx.Command.__init__(self, True, 'Remove Module')
self.fitID = fitID
self.positions = positions
self.recalc = recalc
self.clearTail = clearTail
self.savedSubInfos = None
self.savedModInfos = None
self.savedStateCheckChanges = None
self.savedTail = None
def Do(self):
pyfalog.debug('Doing removal of local modules from positions {} on fit {}'.format(self.positions, self.fitID))
@@ -40,6 +42,9 @@ class CalcRemoveLocalModulesCommand(wx.Command):
if len(self.savedSubInfos) == 0 and len(self.savedModInfos) == 0:
return False
if self.clearTail:
self.savedTail = fit.clearTail()
if self.recalc:
# Need to flush because checkStates sometimes relies on module->fit
# relationship via .owner attribute, which is handled by SQLAlchemy
@@ -76,6 +81,7 @@ class CalcRemoveLocalModulesCommand(wx.Command):
if not any(results):
return False
restoreCheckedStates(fit, self.savedStateCheckChanges)
restoreRemovedDummies(fit, self.savedTail)
return True
@property

View File

@@ -39,7 +39,17 @@ class CalcReplaceLocalModuleCommand(wx.Command):
if newMod.slot != oldMod.slot:
return False
# Dummy it out in case the next bit fails
fit.modules.free(self.position)
fit.modules.replace(self.position, newMod)
if newMod not in fit.modules:
pyfalog.warning('Failed to replace in list')
self.Undo()
return False
if self.recalc:
# Need to flush because checkStates sometimes relies on module->fit
# relationship via .owner attribute, which is handled by SQLAlchemy
eos.db.flush()
sFit.recalc(fit)
self.savedStateCheckChanges = sFit.checkStates(fit, newMod)
if not self.ignoreRestrictions and not newMod.fits(fit):
pyfalog.warning('Module does not fit')
self.Undo()
@@ -52,17 +62,6 @@ class CalcReplaceLocalModuleCommand(wx.Command):
pyfalog.warning('Invalid charge')
self.Undo()
return False
fit.modules.replace(self.position, newMod)
if newMod not in fit.modules:
pyfalog.warning('Failed to replace in list')
self.Undo()
return False
if self.recalc:
# Need to flush because checkStates sometimes relies on module->fit
# relationship via .owner attribute, which is handled by SQLAlchemy
eos.db.flush()
sFit.recalc(fit)
self.savedStateCheckChanges = sFit.checkStates(fit, newMod)
return True
def Undo(self):

View File

@@ -353,6 +353,8 @@ def restoreCheckedStates(fit, stateInfo, ignoreModPoss=()):
def restoreRemovedDummies(fit, dummyInfo):
if dummyInfo is None:
return
# Need this to properly undo the case when removal of subsystems removes dummy slots
for position in sorted(dummyInfo):
slot = dummyInfo[position]

View File

@@ -80,8 +80,8 @@ class MainMenuBar(wx.MenuBar):
fitMenu = wx.Menu()
self.Append(fitMenu, "Fi&t")
fitMenu.Append(wx.ID_UNDO)
fitMenu.Append(wx.ID_REDO)
fitMenu.Append(wx.ID_UNDO, "&Undo\tCTRL+Z", "Undo the most recent action")
fitMenu.Append(wx.ID_REDO, "&Redo\tCTRL+Y", "Redo the most recent undone action")
fitMenu.AppendSeparator()
fitMenu.Append(wx.ID_COPY, "&To Clipboard\tCTRL+C", "Export a fit to the clipboard")

View File

@@ -13,6 +13,7 @@ from eos.db.gamedata.queries import getAttributeInfo, getItem
from gui.auxFrame import AuxiliaryFrame
from gui.bitmap_loader import BitmapLoader
from gui.marketBrowser import SearchBox
from service.fit import Fit
from service.market import Market
@@ -170,12 +171,15 @@ class ItemView(d.Display):
d.Display.__init__(self, parent)
self.activeItems = []
self.searchTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.scheduleSearch, self.searchTimer)
self.searchBox = parent.Parent.Parent.searchBox
# Bind search actions
self.searchBox.Bind(SBox.EVT_TEXT_ENTER, self.scheduleSearch)
self.searchBox.Bind(SBox.EVT_SEARCH_BTN, self.scheduleSearch)
self.searchBox.Bind(SBox.EVT_CANCEL_BTN, self.clearSearch)
self.searchBox.Bind(SBox.EVT_TEXT, self.scheduleSearch)
self.searchBox.Bind(SBox.EVT_TEXT, self.delaySearch)
self.update(Market.getInstance().getItemsWithOverrides())
@@ -188,12 +192,17 @@ class ItemView(d.Display):
if updateDisplay:
self.update(Market.getInstance().getItemsWithOverrides())
def delaySearch(self, evt):
sFit = Fit.getInstance()
self.searchTimer.Stop()
self.searchTimer.Start(sFit.serviceFittingOptions["marketSearchDelay"], True)
def scheduleSearch(self, event=None):
sMkt = Market.getInstance()
search = self.searchBox.GetLineText(0)
# Make sure we do not count wildcard as search symbol
realsearch = search.replace("*", "")
# Make sure we do not count wildcards as search symbol
realsearch = search.replace('*', '').replace('?', '')
# Show nothing if query is too short
if len(realsearch) < 3:
self.clearSearch()
@@ -218,7 +227,8 @@ class ItemView(d.Display):
return not isFittable, catname, mktgrpid, parentname, metatab, metalvl, item.name
def populateSearch(self, items):
def populateSearch(self, itemIDs):
items = Market.getItems(itemIDs)
self.update(items)
def populate(self, items):

View File

@@ -1,5 +1,7 @@
import math
from eos.utils.round import roundToPrec, roundDec
def formatAmount(val, prec=3, lowest=0, highest=0, currency=False, forceSign=False, unitName=None):
"""
@@ -97,29 +99,3 @@ def formatAmount(val, prec=3, lowest=0, highest=0, currency=False, forceSign=Fal
else:
result = "{}{} {}{}".format(sign, mantissa, suffix, unitName)
return result
def roundToPrec(val, prec, nsValue=None):
"""
nsValue: custom value which should be used to determine normalization shift
"""
# We're not rounding integers anyway
# Also make sure that we do not ask to calculate logarithm of zero
if int(val) == val:
return int(val)
roundFactor = int(prec - math.floor(math.log10(abs(val if nsValue is None else nsValue))) - 1)
# But we don't want to round integers
if roundFactor < 0:
roundFactor = 0
# Do actual rounding
val = round(val, roundFactor)
# Make sure numbers with .0 part designating float don't get through
if int(val) == val:
val = int(val)
return val
def roundDec(val, prec):
if int(val) == val:
return int(val)
return round(val, prec)

View File

@@ -226,18 +226,6 @@ def main(old, new, groups=True, effects=True, attributes=True, renames=True):
effectSet.add(effectID)
if attributes:
# Add base attributes to our data
query = 'SELECT it.typeID, it.mass, it.capacity, it.volume FROM invtypes AS it'
cursor.execute(query)
for row in cursor:
itemid = row[0]
if itemid in dictionary:
attrdict = dictionary[itemid][2]
# Add base attributes: mass (4), capacity (38) and volume (161)
attrdict[4] = row[1]
attrdict[38] = row[2]
attrdict[161] = row[3]
# Add attribute data for other attributes
query = 'SELECT dta.typeID, dta.attributeID, dta.value FROM dgmtypeattribs AS dta'
cursor.execute(query)

View File

@@ -242,8 +242,8 @@ class Character:
return eos.db.getCharacterList()
@staticmethod
def getCharacter(charID):
char = eos.db.getCharacter(charID)
def getCharacter(identity):
char = eos.db.getCharacter(identity)
return char
def saveCharacter(self, charID):

View File

@@ -57,6 +57,7 @@ class PortMultiBuyOptions(IntEnum):
CARGO = 2
LOADED_CHARGES = 3
OPTIMIZE_PRICES = 4
BOOSTERS = 5
@unique
@@ -68,6 +69,7 @@ class PortEftOptions(IntEnum):
MUTATIONS = 2
LOADED_CHARGES = 3
CARGO = 4
BOOSTERS = 5
@unique

View File

@@ -3,7 +3,7 @@ Conversion pack for April 2020 release
"""
CONVERSIONS = {
# Renamed items, extracted via diff file (TODO: redo when patch hits, add converted items)
# Renamed items, extracted via diff file
"Adaptive Invulnerability Field I": "Adaptive Invulnerability Shield Hardener I",
"Gistum C-Type Adaptive Invulnerability Field": "Gistum C-Type Adaptive Invulnerability Shield Hardener",
"Adaptive Invulnerability Field II": "Adaptive Invulnerability Shield Hardener II",

View File

@@ -354,8 +354,8 @@ class Fit:
if not mod.isEmpty:
mod.fits(fit)
# Check that the states of all modules are valid
self.checkStates(fit, None)
# Check that the states of all modules are valid
self.checkStates(fit, None)
eos.db.commit()
fit.inited = True

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
# This is the default Pyfa jargon file.
# This is a Pyfa jargon file for user definitions.
#
# It is essentially a giant set of find/replace statements in order to translate
# abbreviated Eve community terms into more useful full terms. It is intended
@@ -11,11 +11,25 @@
#
# Syntax:
#
# abbreviation: full name
# abbreviation:
# - abbreviation
# - replacement 1
# - replacement 2
#
# The default jargon definitions are stored in Pyfa itself as well, and are
# listed here for convenience overriding them. To disable a jargon definition,
# set it as an empty string. For example, if you do not want "web" to return
# anything containing "stasis":
# If you do not want abbreviation to expand into itself, just do not add corresponding
# entry. It might make sense for abbreviations like:
#
# web: ""
# lse:
# - large shield extender
#
# If you add "lse" as first entry, it will return too many entries you do not want to see,
# like all items which have word "pulse".
#
# The default jargon definitions are stored in pyfa/service/jargon/defaults.yaml. Definitions
# stored in this file have priority. Should you wish to disable any of default definitions,
# simply add entry like:
#
# abbreviation:
# - abbreviation
#

View File

@@ -19,6 +19,7 @@
class Jargon:
def __init__(self, rawdata: dict):
self._rawdata = rawdata
@@ -31,15 +32,12 @@ class Jargon:
def get_rawdata(self) -> dict:
return self._rawdata
def apply(self, query):
query_words = query.split()
def apply(self, query_words):
parts = []
for word in query_words:
replacement = self.get(word)
if replacement:
parts.append(replacement)
replacements = self.get(word)
if replacements:
parts.append('({})'.format('|'.join(replacements)))
else:
parts.append(word)
return ' '.join(parts)
return parts

View File

@@ -24,13 +24,13 @@ import yaml
from .jargon import Jargon
from .resources import DEFAULT_DATA, DEFAULT_HEADER
JARGON_PATH = os.path.join(config.savePath, 'jargon.yaml') if config.savePath is not None else None
USER_JARGON_PATH = os.path.join(config.savePath, 'user_jargon.yaml') if config.savePath is not None else None
class JargonLoader:
def __init__(self, jargon_path: str):
self.jargon_path = jargon_path
self._jargon_mtime = 0 # type: int
def __init__(self):
self._user_jargon_mtime = 0 # type: int
self._jargon = None # type: Jargon
def get_jargon(self) -> Jargon:
@@ -39,49 +39,39 @@ class JargonLoader:
return self._jargon
def _is_stale(self):
return (not self._jargon or not self._jargon_mtime or
self.jargon_mtime != self._get_jargon_file_mtime())
return (not self._jargon or not self._user_jargon_mtime or
self.jargon_mtime != self._get_user_jargon_mtime())
def _load_jargon(self):
jargondata = yaml.load(DEFAULT_DATA, Loader=yaml.SafeLoader)
if JARGON_PATH is not None:
with open(JARGON_PATH) as f:
if USER_JARGON_PATH is not None and os.path.isfile(USER_JARGON_PATH):
with open(USER_JARGON_PATH) as f:
userdata = yaml.load(f, Loader=yaml.SafeLoader)
jargondata.update(userdata)
self.jargon_mtime = self._get_jargon_file_mtime()
if userdata:
jargondata.update(userdata)
self.jargon_mtime = self._get_user_jargon_mtime()
self._jargon = Jargon(jargondata)
def _get_jargon_file_mtime(self) -> int:
if self.jargon_path is None or not os.path.exists(self.jargon_path):
def _get_user_jargon_mtime(self) -> int:
if USER_JARGON_PATH is None or not os.path.isfile(USER_JARGON_PATH):
return 0
return os.stat(self.jargon_path).st_mtime
return os.stat(USER_JARGON_PATH).st_mtime
@staticmethod
def init_user_jargon(jargon_path):
values = yaml.load(DEFAULT_DATA, Loader=yaml.SafeLoader)
# Disabled for issue/1533; do not overwrite existing user config
# if os.path.exists(jargon_path):
# with open(jargon_path) as f:
# custom_values = yaml.load(f)
# if custom_values:
# values.update(custom_values)
if not os.path.exists(jargon_path):
with open(jargon_path, 'w') as f:
f.write(DEFAULT_HEADER)
f.write('\n\n')
yaml.dump(values, stream=f, default_flow_style=False)
_instance = None
@staticmethod
def instance(jargon_path=None):
def instance():
if not JargonLoader._instance:
jargon_path = jargon_path or JARGON_PATH
JargonLoader._instance = JargonLoader(jargon_path)
JargonLoader._instance = JargonLoader()
return JargonLoader._instance
if JARGON_PATH is not None:
JargonLoader.init_user_jargon(JARGON_PATH)
if USER_JARGON_PATH is not None:
JargonLoader.init_user_jargon(USER_JARGON_PATH)

View File

@@ -18,6 +18,7 @@
# ===============================================================================
import queue
import re
import threading
from collections import OrderedDict
from itertools import chain
@@ -41,6 +42,10 @@ pyfalog = Logger(__name__)
mktRdy = threading.Event()
class RegexTokenizationError(Exception):
pass
class ShipBrowserWorkerThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
@@ -90,13 +95,14 @@ class ShipBrowserWorkerThread(threading.Thread):
class SearchWorkerThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.name = "SearchWorker"
self.jargonLoader = JargonLoader.instance()
# load the jargon while in an out-of-thread context, to spot any problems while in the main thread
self.jargonLoader.get_jargon()
self.jargonLoader.get_jargon().apply('test string')
self.jargonLoader.get_jargon().apply('test string'.split())
self.running = True
def run(self):
@@ -138,31 +144,27 @@ class SearchWorkerThread(threading.Thread):
else:
filters = [None]
jargon_request = self.jargonLoader.get_jargon().apply(request)
if request.strip().lower().startswith('re:'):
requestTokens = self._prepareRequestRegex(request[3:])
else:
requestTokens = self._prepareRequestNormal(request)
requestTokens = self.jargonLoader.get_jargon().apply(requestTokens)
all_results = set()
if len(request) >= config.minItemSearchLength:
if len(' '.join(requestTokens)) >= config.minItemSearchLength:
for filter_ in filters:
regular_results = eos.db.searchItems(
request, where=filter_,
filtered_results = eos.db.searchItemsRegex(
requestTokens, where=filter_,
join=(types_Item.group, types_Group.category),
eager=("group.category", "metaGroup"))
all_results.update(regular_results)
all_results.update(filtered_results)
if len(jargon_request) >= config.minItemSearchLength:
for filter_ in filters:
jargon_results = eos.db.searchItems(
jargon_request, where=filter_,
join=(types_Item.group, types_Group.category),
eager=("group.category", "metaGroup"))
all_results.update(jargon_results)
items = set()
item_IDs = set()
# Return only published items, consult with Market service this time
for item in all_results:
if sMkt.getPublicityByItem(item):
items.add(item)
wx.CallAfter(callback, list(items))
item_IDs.add(item.ID)
wx.CallAfter(callback, sorted(item_IDs))
def scheduleSearch(self, text, callback, filterName=None):
self.cv.acquire()
@@ -173,6 +175,66 @@ class SearchWorkerThread(threading.Thread):
def stop(self):
self.running = False
def _prepareRequestNormal(self, request):
# Escape regexp-specific symbols, and un-escape whitespaces
request = re.escape(request)
request = re.sub(r'\\(?P<ws>\s+)', '\g<ws>', request)
# Imitate wildcard search
request = re.sub(r'\\\*', r'\\w*', request)
request = re.sub(r'\\\?', r'\\w?', request)
tokens = request.split()
return tokens
def _prepareRequestRegex(self, request):
roundLvl = 0
squareLvl = 0
nextEscaped = False
tokens = []
currentToken = ''
def verifyErrors():
if squareLvl not in (0, 1):
raise RegexTokenizationError('Square braces level is {}'.format(squareLvl))
if roundLvl < 0:
raise RegexTokenizationError('Round braces level is {}'.format(roundLvl))
try:
for char in request:
thisEscaped = nextEscaped
nextEscaped = False
if thisEscaped:
currentToken += char
elif char == '\\':
currentToken += char
nextEscaped = True
elif char == '[':
currentToken += char
squareLvl += 1
elif char == ']':
currentToken += char
squareLvl -= 1
elif char == '(' and squareLvl == 0:
currentToken += char
roundLvl += 1
elif char == ')' and squareLvl == 0:
currentToken += char
roundLvl -= 1
elif char.isspace() and roundLvl == squareLvl == 0:
if currentToken:
tokens.append(currentToken)
currentToken = ''
else:
currentToken += char
verifyErrors()
else:
if currentToken:
tokens.append(currentToken)
# Treat request as normal string if regex tokenization fails
except RegexTokenizationError:
tokens = self._prepareRequestNormal(request)
return tokens
class Market:
instance = None
@@ -426,6 +488,11 @@ class Market:
return item
@staticmethod
def getItems(itemIDs, eager=None):
items = eos.db.getItems(itemIDs, eager=eager)
return items
def getGroup(self, identity, *args, **kwargs):
"""Get group by its ID or name"""
if isinstance(identity, types_Group):

View File

@@ -47,7 +47,8 @@ pyfalog = Logger(__name__)
EFT_OPTIONS = (
(PortEftOptions.LOADED_CHARGES, 'Loaded Charges', 'Export charges loaded into modules', True),
(PortEftOptions.MUTATIONS, 'Mutated Attributes', 'Export mutated modules\' stats', True),
(PortEftOptions.IMPLANTS, 'Implants && Boosters', 'Export implants and boosters', True),
(PortEftOptions.IMPLANTS, 'Implants', 'Export implants', True),
(PortEftOptions.BOOSTERS, 'Boosters', 'Export boosters', True),
(PortEftOptions.CARGO, 'Cargo', 'Export cargo hold contents', True))
@@ -115,16 +116,17 @@ def exportEft(fit, options, callback):
sections.append('\n\n'.join(minionSection))
# Section 3: implants, boosters
charSection = []
if options[PortEftOptions.IMPLANTS]:
charSection = []
implantExport = exportImplants(fit.implants)
if implantExport:
charSection.append(implantExport)
if options[PortEftOptions.BOOSTERS]:
boosterExport = exportBoosters(fit.boosters)
if boosterExport:
charSection.append(boosterExport)
if charSection:
sections.append('\n\n'.join(charSection))
if charSection:
sections.append('\n\n'.join(charSection))
# Section 4: cargo
if options[PortEftOptions.CARGO]:

View File

@@ -24,7 +24,8 @@ from service.price import Price as sPrc
MULTIBUY_OPTIONS = (
(PortMultiBuyOptions.LOADED_CHARGES, 'Loaded Charges', 'Export charges loaded into modules', True),
(PortMultiBuyOptions.IMPLANTS, 'Implants && Boosters', 'Export implants and boosters', False),
(PortMultiBuyOptions.IMPLANTS, 'Implants', 'Export implants', False),
(PortMultiBuyOptions.BOOSTERS, 'Boosters', 'Export boosters', False),
(PortMultiBuyOptions.CARGO, 'Cargo', 'Export cargo contents', True),
(PortMultiBuyOptions.OPTIMIZE_PRICES, 'Optimize Prices', 'Replace items by cheaper alternatives', False),
)
@@ -56,6 +57,7 @@ def exportMultiBuy(fit, options, callback):
for implant in fit.implants:
_addItem(itemAmounts, implant.item)
if options[PortMultiBuyOptions.BOOSTERS]:
for booster in fit.boosters:
_addItem(itemAmounts, booster.item)

View File

@@ -78272,5 +78272,36 @@
"propulsionChance": 0,
"published": 0,
"rangeChance": 0
},
"8029": {
"disallowAutoRepeat": 0,
"effectCategory": 0,
"effectID": 8029,
"effectName": "roleBonus7CapBoosterGroupRestriction",
"electronicChance": 0,
"isAssistance": 0,
"isOffensive": 0,
"isWarpSafe": 0,
"modifierInfo": [
{
"domain": "shipID",
"func": "LocationGroupModifier",
"groupID": 76,
"modifiedAttributeID": 1544,
"modifyingAttributeID": 793,
"operation": 7
},
{
"domain": "shipID",
"func": "LocationGroupModifier",
"groupID": 76,
"modifiedAttributeID": 978,
"modifyingAttributeID": 793,
"operation": 7
}
],
"propulsionChance": 0,
"published": 0,
"rangeChance": 0
}
}

View File

@@ -67718,6 +67718,17 @@
1
]
},
"33682": {
"3380": [
5
],
"11446": [
1
],
"11453": [
1
]
},
"33683": {
"17940": [
5
@@ -88378,6 +88389,96 @@
5
]
},
"54410": {
"3380": [
4
]
},
"54411": {
"3380": [
4
]
},
"54412": {
"3380": [
4
]
},
"54413": {
"3380": [
4
]
},
"54414": {
"3380": [
4
]
},
"54415": {
"3380": [
4
]
},
"54416": {
"3380": [
4
]
},
"54417": {
"3380": [
4
]
},
"54418": {
"3380": [
4
]
},
"54419": {
"3380": [
4
]
},
"54420": {
"3380": [
4
]
},
"54421": {
"3380": [
4
]
},
"54422": {
"3380": [
4
]
},
"54423": {
"3380": [
4
]
},
"54424": {
"3380": [
4
]
},
"54425": {
"3380": [
4
]
},
"54426": {
"3380": [
4
]
},
"54427": {
"3380": [
4
]
},
"54534": {
"3411": [
1

File diff suppressed because it is too large Load Diff

View File

@@ -306517,6 +306517,7 @@
"descriptionID": 294485,
"graphicID": 20483,
"groupID": 100,
"isDynamicType": false,
"marketGroupID": 839,
"mass": 10000.0,
"metaGroupID": 4,
@@ -325067,10 +325068,12 @@
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 1965,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 34797,
"typeName": "Raven Guristas SKIN",
"typeNameID": 305802,
@@ -397208,6 +397211,7 @@
"factionID": 500003,
"graphicID": 21252,
"groupID": 1538,
"isDynamicType": false,
"isisGroupID": 33,
"marketGroupID": 2272,
"mass": 1310000000.0,
@@ -397234,6 +397238,7 @@
"factionID": 500001,
"graphicID": 21254,
"groupID": 1538,
"isDynamicType": false,
"isisGroupID": 33,
"marketGroupID": 2273,
"mass": 1300000000.0,
@@ -397260,6 +397265,7 @@
"factionID": 500002,
"graphicID": 21256,
"groupID": 1538,
"isDynamicType": false,
"isisGroupID": 33,
"marketGroupID": 2275,
"mass": 1260000000.0,
@@ -397285,6 +397291,7 @@
"factionID": 500004,
"graphicID": 21255,
"groupID": 1538,
"isDynamicType": false,
"isisGroupID": 33,
"marketGroupID": 2274,
"mass": 1250000000.0,
@@ -446364,9 +446371,9 @@
"marketGroupID": 1965,
"mass": 0.0,
"portionSize": 1,
"published": false,
"published": true,
"raceID": 1,
"radius": 1,
"radius": 1.0,
"typeID": 40773,
"typeName": "Scorpion Guristas SKIN",
"typeNameID": 514687,
@@ -468011,25 +468018,26 @@
"wreckTypeID": 26516
},
"42133": {
"basePrice": 1700000000,
"capacity": 2800,
"basePrice": 1700000000.0,
"capacity": 2800.0,
"graphicID": 21355,
"groupID": 1538,
"isDynamicType": false,
"isisGroupID": 33,
"marketGroupID": 2274,
"mass": 1250000000,
"mass": 1250000000.0,
"metaGroupID": 4,
"metaLevel": 8,
"portionSize": 1,
"published": false,
"raceID": 8,
"radius": 3000,
"radius": 3000.0,
"soundID": 20073,
"techLevel": 1,
"typeID": 42133,
"typeName": "Venerable",
"typeNameID": 517446,
"volume": 17020000,
"volume": 17020000.0,
"wreckTypeID": 40641
},
"42134": {
@@ -601510,7 +601518,7 @@
"54228": {
"basePrice": 0.0,
"capacity": 0.0,
"description": "This corpse once belonged to a deep-cover informant collecting information for CONCORD's Directive Enforcement Division from within the Guristas Pirates. Apparently their cover was blown somehow.",
"description": "This corpse once belonged to a deep-cover informant collecting information for CONCORD's Directive Enforcement Department from within the Guristas Pirates. Apparently their cover was blown somehow.",
"descriptionID": 561612,
"groupID": 526,
"iconID": 2855,
@@ -601845,7 +601853,7 @@
"graphicID": 3819,
"groupID": 1568,
"isDynamicType": false,
"mass": 950000.0,
"mass": 1500000.0,
"portionSize": 1,
"published": false,
"raceID": 32,
@@ -601863,7 +601871,7 @@
"graphicID": 3819,
"groupID": 1568,
"isDynamicType": false,
"mass": 950000.0,
"mass": 1500000.0,
"portionSize": 1,
"published": false,
"raceID": 32,
@@ -601881,7 +601889,7 @@
"graphicID": 3819,
"groupID": 1568,
"isDynamicType": false,
"mass": 950000.0,
"mass": 1500000.0,
"portionSize": 1,
"published": false,
"raceID": 32,
@@ -601899,7 +601907,7 @@
"graphicID": 3819,
"groupID": 1568,
"isDynamicType": false,
"mass": 950000.0,
"mass": 1500000.0,
"portionSize": 1,
"published": false,
"raceID": 32,
@@ -601917,7 +601925,7 @@
"graphicID": 1968,
"groupID": 1568,
"isDynamicType": false,
"mass": 1080000.0,
"mass": 1480000.0,
"portionSize": 1,
"published": false,
"raceID": 2,
@@ -601935,7 +601943,7 @@
"graphicID": 1827,
"groupID": 1568,
"isDynamicType": false,
"mass": 1080000.0,
"mass": 1100000.0,
"portionSize": 1,
"published": false,
"raceID": 2,
@@ -601953,7 +601961,7 @@
"graphicID": 1828,
"groupID": 1568,
"isDynamicType": false,
"mass": 1080000.0,
"mass": 1056000.0,
"portionSize": 1,
"published": false,
"raceID": 2,
@@ -601971,7 +601979,7 @@
"graphicID": 1830,
"groupID": 1568,
"isDynamicType": false,
"mass": 1080000.0,
"mass": 1113000.0,
"portionSize": 1,
"published": false,
"raceID": 2,
@@ -601989,7 +601997,7 @@
"graphicID": 1831,
"groupID": 1568,
"isDynamicType": false,
"mass": 1080000.0,
"mass": 981000.0,
"portionSize": 1,
"published": false,
"raceID": 2,
@@ -602022,10 +602030,10 @@
"54287": {
"basePrice": 0.0,
"capacity": 450.0,
"graphicID": 24531,
"groupID": 1568,
"graphicID": 20200,
"groupID": 1664,
"isDynamicType": false,
"mass": 1080000.0,
"mass": 1900000.0,
"portionSize": 1,
"published": false,
"raceID": 2,
@@ -602061,7 +602069,7 @@
"graphicID": 1826,
"groupID": 1665,
"isDynamicType": false,
"mass": 11910000.0,
"mass": 13190000.0,
"portionSize": 1,
"published": false,
"raceID": 1,
@@ -602079,7 +602087,7 @@
"graphicID": 1824,
"groupID": 1665,
"isDynamicType": false,
"mass": 11910000.0,
"mass": 9600000.0,
"portionSize": 1,
"published": false,
"raceID": 1,
@@ -602118,7 +602126,7 @@
"54292": {
"basePrice": 45412.0,
"capacity": 0.0,
"description": "Boosts shield resistance against EM damage.<br><br>Penalty: Using more than one type of this module, or similar modules that affect the same resistance type, will result in a penalty to the boost you get on that type of resistance",
"description": "This capsule resembles the ones that house and protect capsuleers while they control their starships. However it is broadcasting Guristas transponder codes instead of capsuleer identification codes, and close inspection suggests that its exterior hull has been painted white. Scans indicate that a single life form is on board.",
"descriptionID": 561840,
"groupID": 77,
"iconID": 20948,
@@ -602323,7 +602331,7 @@
"marketGroupID": 1400,
"mass": 0.5,
"portionSize": 1,
"published": false,
"published": true,
"radius": 1.0,
"typeID": 54309,
"typeName": "Men's 'Fatal Elite' Combat Boots",
@@ -602403,6 +602411,36 @@
"typeNameID": 561983,
"volume": 0.1
},
"54339": {
"basePrice": 0.0,
"capacity": 0.0,
"graphicID": 10026,
"groupID": 227,
"isDynamicType": false,
"mass": 0.0,
"portionSize": 1,
"published": false,
"radius": 1.0,
"typeID": 54339,
"typeName": "Hunt YC112 Boss Spawner",
"typeNameID": 562072,
"volume": 0.0
},
"54340": {
"basePrice": 0.0,
"capacity": 0.0,
"graphicID": 10026,
"groupID": 227,
"isDynamicType": false,
"mass": 0.0,
"portionSize": 1,
"published": false,
"radius": 1.0,
"typeID": 54340,
"typeName": "Huntmaster YC112 Boss Spawner",
"typeNameID": 562074,
"volume": 0.0
},
"54341": {
"basePrice": 0.0,
"capacity": 0.0,
@@ -604772,7 +604810,7 @@
"54534": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the max velocity of drones and fighters controlled by the pilot.\r\n\r\n1% increase in drone and fighter max velocity.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the max velocity of drones and fighters controlled by the pilot.\r\n\r\n1% increase in drone and fighter max velocity.",
"descriptionID": 562798,
"groupID": 739,
"iconID": 2224,
@@ -604791,7 +604829,7 @@
"54535": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the max velocity of drones and fighters controlled by the pilot.\r\n\r\n3% increase in drone and fighter max velocity.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the max velocity of drones and fighters controlled by the pilot.\r\n\r\n3% increase in drone and fighter max velocity.",
"descriptionID": 562800,
"groupID": 739,
"iconID": 2224,
@@ -604810,7 +604848,7 @@
"54536": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the max velocity of drones and fighters controlled by the pilot.\r\n\r\n5% increase in drone and fighter max velocity.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the max velocity of drones and fighters controlled by the pilot.\r\n\r\n5% increase in drone and fighter max velocity.",
"descriptionID": 562802,
"groupID": 739,
"iconID": 2224,
@@ -604829,7 +604867,7 @@
"54537": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the optimal range of drones and fighters controlled by the pilot.\r\n\r\n1% increase in drone and fighter optimal range.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the optimal range of drones and fighters controlled by the pilot.\r\n\r\n1% increase in drone and fighter optimal range.",
"descriptionID": 562804,
"groupID": 739,
"iconID": 2224,
@@ -604848,7 +604886,7 @@
"54538": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the optimal range of drones and fighters controlled by the pilot.\r\n\r\n3% increase in drone and fighter optimal range.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the optimal range of drones and fighters controlled by the pilot.\r\n\r\n3% increase in drone and fighter optimal range.",
"descriptionID": 562806,
"groupID": 739,
"iconID": 2224,
@@ -604867,7 +604905,7 @@
"54539": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the optimal range of drones and fighters controlled by the pilot.\r\n\r\n5% increase in drone and fighter optimal range.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the optimal range of drones and fighters controlled by the pilot.\r\n\r\n5% increase in drone and fighter optimal range.",
"descriptionID": 562808,
"groupID": 739,
"iconID": 2224,
@@ -604886,7 +604924,7 @@
"54540": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the effectiveness of repair drones controlled by the pilot.\r\n\r\n1% increase in drone repair amount.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the effectiveness of repair drones controlled by the pilot.\r\n\r\n1% increase in drone repair amount.",
"descriptionID": 562810,
"groupID": 739,
"iconID": 2224,
@@ -604924,7 +604962,7 @@
"54542": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the effectiveness of repair drones controlled by the pilot.\r\n\r\n5% increase in drone repair amount. ",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the effectiveness of repair drones controlled by the pilot.\r\n\r\n5% increase in drone repair amount. ",
"descriptionID": 562814,
"groupID": 739,
"iconID": 2224,
@@ -604943,7 +604981,7 @@
"54543": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the durability of drones and fighters controlled by the pilot.\r\n\r\n1% increase in drone and fighter hit points.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the durability of drones and fighters controlled by the pilot.\r\n\r\n1% increase in drone and fighter hit points.",
"descriptionID": 562816,
"groupID": 739,
"iconID": 2224,
@@ -604962,7 +605000,7 @@
"54544": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the durability of drones and fighters controlled by the pilot.\r\n\r\n3% increase in drone and fighter hit points.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the durability of drones and fighters controlled by the pilot.\r\n\r\n3% increase in drone and fighter hit points.",
"descriptionID": 562818,
"groupID": 739,
"iconID": 2224,
@@ -604981,7 +605019,7 @@
"54545": {
"basePrice": 200000.0,
"capacity": 0.0,
"description": "Named after the mythical aquatic worm from ancient mythology, this neural interface upgrade boosts the durability of drones and fighters controlled by the pilot.\r\n\r\n5% increase in drone and fighter hit points.",
"description": "Named after an aquatic demon worm from ancient Minmatar mythology, this neural interface upgrade boosts the durability of drones and fighters controlled by the pilot.\r\n\r\n5% increase in drone and fighter hit points.",
"descriptionID": 562820,
"groupID": 739,
"iconID": 2224,
@@ -605088,6 +605126,174 @@
"typeNameID": 562843,
"volume": 1.0
},
"54552": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 2003,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54552,
"typeName": "Condor Guristas SKIN",
"typeNameID": 562867,
"volume": 0.01
},
"54553": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 2003,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54553,
"typeName": "Griffin Guristas SKIN",
"typeNameID": 562868,
"volume": 0.01
},
"54554": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 2003,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54554,
"typeName": "Kestrel Guristas SKIN",
"typeNameID": 562869,
"volume": 0.01
},
"54555": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 2056,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54555,
"typeName": "Kitsune Guristas SKIN",
"typeNameID": 562870,
"volume": 0.01
},
"54556": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 1995,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54556,
"typeName": "Corax Guristas SKIN",
"typeNameID": 562871,
"volume": 0.01
},
"54557": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 2143,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54557,
"typeName": "Stork Guristas SKIN",
"typeNameID": 562872,
"volume": 0.01
},
"54558": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 1991,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54558,
"typeName": "Caracal Guristas SKIN",
"typeNameID": 562873,
"volume": 0.01
},
"54559": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 1991,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54559,
"typeName": "Blackbird Guristas SKIN",
"typeNameID": 562874,
"volume": 0.01
},
"54560": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 2082,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54560,
"typeName": "Rook Guristas SKIN",
"typeNameID": 562875,
"volume": 0.01
},
"54561": {
"basePrice": 0.0,
"capacity": 0.0,
"groupID": 1950,
"marketGroupID": 1957,
"mass": 0.0,
"portionSize": 1,
"published": true,
"raceID": 1,
"radius": 1.0,
"typeID": 54561,
"typeName": "Drake Guristas SKIN",
"typeNameID": 562876,
"volume": 0.01
},
"54562": {
"basePrice": 250000.0,
"capacity": 0.0,
"description": "This appears to be a heavily modified version of a standard Gallente drone control unit. Close inspection reveals that it has been modified to allow control of much larger and more sophisticated drones than those available to the empires.",
"descriptionID": 562881,
"groupID": 1676,
"iconID": 2890,
"marketGroupID": 2155,
"mass": 1.0,
"portionSize": 1,
"published": true,
"raceID": 32,
"radius": 1.0,
"typeID": 54562,
"typeName": "Korako's Modified Drone Control Unit",
"typeNameID": 562880,
"volume": 0.1
},
"54563": {
"basePrice": 0.0,
"capacity": 0.0,

View File

@@ -1,10 +1,10 @@
[
{
"field_name": "client_build",
"field_value": 1702461
"field_value": 1706308
},
{
"field_name": "dump_time",
"field_value": 1586271118
"field_value": 1586939555
}
]

View File

@@ -9311,6 +9311,9 @@
{
"number": "200%",
"text": "bonus to Command Burst area of effect range"
},
{
"text": "·Can only online one Capacitor Booster module"
}
],
"header": "Role Bonus:"
@@ -9362,6 +9365,9 @@
{
"number": "5x",
"text": "penalty to Entosis Link duration"
},
{
"text": "·Can only online one Capacitor Booster module"
}
],
"header": "Role Bonus:"
@@ -9413,6 +9419,9 @@
{
"number": "5x",
"text": "penalty to Entosis Link duration"
},
{
"text": "·Can only online one Capacitor Booster module"
}
],
"header": "Role Bonus:"
@@ -9461,6 +9470,9 @@
"number": "200%",
"text": "bonus to Logistics Drone transfer amount"
},
{
"text": "·Can only online one Capacitor Booster module"
},
{
"number": "5x",
"text": "penalty to Entosis Link duration"
@@ -13581,6 +13593,9 @@
"number": "200%",
"text": "bonus to Command Burst area of effect range"
},
{
"text": "·Can only online one Capacitor Booster module"
},
{
"number": "5x",
"text": "penalty to Entosis Link cycle time"
@@ -14033,6 +14048,9 @@
{
"number": "5x",
"text": "penalty to Entosis Link duration"
},
{
"text": "·Can only online one Capacitor Booster module"
}
],
"header": "Role Bonus:"

View File

@@ -1 +1 @@
version: v2.19.1dev1
version: v2.20.3