Compare commits
44 Commits
v2.19.1dev
...
v2.20.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
855fafa94d | ||
|
|
4e10335ae7 | ||
|
|
21ea9ce579 | ||
|
|
ea07bbf4f9 | ||
|
|
8eed6fbe21 | ||
|
|
0859f2fbe9 | ||
|
|
71ba33edeb | ||
|
|
ce80d92b35 | ||
|
|
f17cf9b736 | ||
|
|
98579c774b | ||
|
|
509fa279e7 | ||
|
|
091ee87761 | ||
|
|
c0c20cc92e | ||
|
|
1341f7bca1 | ||
|
|
fe93db1d4b | ||
|
|
5db97ea773 | ||
|
|
1758e4f320 | ||
|
|
1a897c0419 | ||
|
|
32db3e3179 | ||
|
|
d830a8957a | ||
|
|
652ea48223 | ||
|
|
8c25b2b8f5 | ||
|
|
db4c56be8e | ||
|
|
f3bcffe2f9 | ||
|
|
bc5786d099 | ||
|
|
5959fe5daf | ||
|
|
649d338bb1 | ||
|
|
dcb058a718 | ||
|
|
1772bb5e7f | ||
|
|
30bd0adb06 | ||
|
|
44dfcf771c | ||
|
|
a1f8a7a930 | ||
|
|
b22887dfad | ||
|
|
28137fa3f4 | ||
|
|
9cbdc6055d | ||
|
|
fc93c61fcf | ||
|
|
3fa2e7ebd1 | ||
|
|
818628da0c | ||
|
|
adf90a8263 | ||
|
|
362923ac64 | ||
|
|
7d73838ce1 | ||
|
|
b3278ca9ec | ||
|
|
5707914ad5 | ||
|
|
9b697b24d8 |
24
db_update.py
24
db_update.py
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
27
eos/utils/round.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
#
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[
|
||||
{
|
||||
"field_name": "client_build",
|
||||
"field_value": 1702461
|
||||
"field_value": 1706308
|
||||
},
|
||||
{
|
||||
"field_name": "dump_time",
|
||||
"field_value": 1586271118
|
||||
"field_value": 1586939555
|
||||
}
|
||||
]
|
||||
@@ -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:"
|
||||
|
||||
@@ -1 +1 @@
|
||||
version: v2.19.1dev1
|
||||
version: v2.20.3
|
||||
|
||||
Reference in New Issue
Block a user