# =============================================================================== # Copyright (C) 2010 Diego Duclos # # This file is part of pyfa. # # pyfa is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pyfa is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pyfa. If not, see . # =============================================================================== import queue import re import threading from collections import OrderedDict from itertools import chain # noinspection PyPackageRequirements import wx from logbook import Logger from sqlalchemy.sql import or_ import config import eos.db from eos.gamedata import Category as types_Category, Group as types_Group, Item as types_Item, MarketGroup as types_MarketGroup, \ MetaGroup as types_MetaGroup from service import conversions from service.jargon import JargonLoader from service.settings import SettingsProvider from utils.cjk import isStringCjk pyfalog = Logger(__name__) _t = wx.GetTranslation # Event which tells threads dependent on Market that it's initialized mktRdy = threading.Event() class RegexTokenizationError(Exception): pass class ShipBrowserWorkerThread(threading.Thread): def __init__(self): threading.Thread.__init__(self) pyfalog.debug("Initialize ShipBrowserWorkerThread.") self.name = "ShipBrowser" self.running = True def run(self): self.queue = queue.Queue() self.cache = {} # Wait for full market initialization (otherwise there's high risky # this thread will attempt to init Market which is already being inited) mktRdy.wait(5) self.processRequests() def processRequests(self): queue = self.queue cache = self.cache sMkt = Market.getInstance() while True: if not self.running: break try: id_, callback = queue.get() set_ = cache.get(id_) if set_ is None: set_ = sMkt.getShipList(id_) cache[id_] = set_ wx.CallAfter(callback, (id_, set_)) except (KeyboardInterrupt, SystemExit): raise except Exception as e: pyfalog.critical("Callback failed.") pyfalog.critical(e) finally: try: queue.task_done() except (KeyboardInterrupt, SystemExit): raise except Exception as e: pyfalog.critical("Queue task done failed.") pyfalog.critical(e) def stop(self): self.running = False 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'.split()) self.running = True def run(self): self.cv = threading.Condition() self.searchRequest = None self.processSearches() def processSearches(self): cv = self.cv while True: if not self.running: break cv.acquire() while self.searchRequest is None: cv.wait() request, callback, filterName = self.searchRequest self.searchRequest = None cv.release() sMkt = Market.getInstance() if filterName == 'market': # Rely on category data provided by eos as we don't hardcode them much in service filters = [or_( types_Category.name.in_(sMkt.SEARCH_CATEGORIES), types_Group.name.in_(sMkt.SEARCH_GROUPS))] # Used in implant editor elif filterName == 'implants': filters = [types_Category.name == 'Implant'] # Actually not everything, just market search + ships elif filterName == 'everything': filters = [ or_( types_Category.name.in_(sMkt.FIT_CATEGORIES), types_Group.name.in_(sMkt.FIT_GROUPS)), or_( types_Category.name.in_(sMkt.SEARCH_CATEGORIES), types_Group.name.in_(sMkt.SEARCH_GROUPS))] else: filters = [None] 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() joinedTokens = ' '.join(requestTokens) if ( (isStringCjk(joinedTokens) and len(joinedTokens) >= config.minItemSearchLengthCjk) or len(joinedTokens) >= config.minItemSearchLength ): for filter_ in filters: filtered_results = eos.db.searchItemsRegex( requestTokens, where=filter_, join=(types_Item.group, types_Group.category), eager=("group.category", "metaGroup")) all_results.update(filtered_results) item_IDs = set() # Return only published items, consult with Market service this time for item in all_results: if sMkt.getPublicityByItem(item): item_IDs.add(item.ID) wx.CallAfter(callback, sorted(item_IDs)) def scheduleSearch(self, text, callback, filterName=None): self.cv.acquire() self.searchRequest = (text, callback, filterName) self.cv.notify() self.cv.release() 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\s+)', r'\g', 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 def __init__(self): # Init recently used module storage serviceMarketRecentlyUsedModules = {"pyfaMarketRecentlyUsedModules": []} self.serviceMarketRecentlyUsedModules = SettingsProvider.getInstance().getSettings( "pyfaMarketRecentlyUsedModules", serviceMarketRecentlyUsedModules) # Thread which handles search self.searchWorkerThread = SearchWorkerThread() self.searchWorkerThread.daemon = True self.searchWorkerThread.start() # Ship browser helper thread self.shipBrowserWorkerThread = ShipBrowserWorkerThread() self.shipBrowserWorkerThread.daemon = True self.shipBrowserWorkerThread.start() # Items' group overrides self.customGroups = set() # Limited edition ships self.les_grp = types_Group() self.les_grp.ID = -1 self.les_grp.name = "Limited Issue Ships" self.les_grp.displayName = _t("Limited Issue Ships") self.les_grp.published = True ships = self.getCategory("Ship") self.les_grp.category = ships self.les_grp.categoryID = ships.ID self.les_grp.description = "" self.les_grp.icon = None self.ITEMS_FORCEGROUP = { "Capsule" : self.getGroup("Shuttle"), "Opux Luxury Yacht" : self.les_grp, # One of those is wedding present at CCP fanfest, another was hijacked from ISD guy during an event "Silver Magnate" : self.les_grp, # Amarr Championship prize "Gold Magnate" : self.les_grp, # Amarr Championship prize "Armageddon Imperial Issue" : self.les_grp, # Amarr Championship prize "Apocalypse Imperial Issue" : self.les_grp, # Amarr Championship prize "Guardian-Vexor" : self.les_grp, # Illegal rewards for the Gallente Frontier Tour Lines event arc "Megathron Federate Issue" : self.les_grp, # Reward during Crielere event "Raven State Issue" : self.les_grp, # AT4 prize "Tempest Tribal Issue" : self.les_grp, # AT4 prize "Apotheosis" : self.les_grp, # 5th EVE anniversary present "Zephyr" : self.les_grp, # 2010 new year gift "Primae" : self.les_grp, # Promotion of planetary interaction "Council Diplomatic Shuttle" : self.les_grp, # CSM X celebration "Freki" : self.les_grp, # AT7 prize "Mimir" : self.les_grp, # AT7 prize "Utu" : self.les_grp, # AT8 prize "Adrestia" : self.les_grp, # AT8 prize "Echelon" : self.les_grp, # 2011 new year gift "Malice" : self.les_grp, # AT9 prize "Vangel" : self.les_grp, # AT9 prize "Cambion" : self.les_grp, # AT10 prize "Etana" : self.les_grp, # AT10 prize "Chremoas" : self.les_grp, # AT11 prize :( "Moracha" : self.les_grp, # AT11 prize "Stratios Emergency Responder": self.les_grp, # Issued for Somer Blink lottery "Miasmos Quafe Ultra Edition" : self.les_grp, # Gift to people who purchased FF HD stream "InterBus Shuttle" : self.les_grp, "Leopard" : self.les_grp, # 2013 new year gift "Whiptail" : self.les_grp, # AT12 prize "Chameleon" : self.les_grp, # AT12 prize "Victorieux Luxury Yacht" : self.les_grp, # Worlds Collide prize \o/ chinese getting owned "Imp" : self.les_grp, # AT13 prize "Fiend" : self.les_grp, # AT13 prize "Caedes" : self.les_grp, # AT14 prize "Rabisu" : self.les_grp, # AT14 prize "Victor" : self.les_grp, # AT15 prize "Virtuoso" : self.les_grp, # AT15 prize "Hydra" : self.les_grp, # AT16 prize "Tiamat" : self.les_grp, # AT16 prize "Raiju" : self.les_grp, # AT17 prize "Laelaps" : self.les_grp, # AT17 prize "Boobook" : self.les_grp, # 19th EVE anniversary gift "Geri" : self.les_grp, # AT18 prize "Bestla" : self.les_grp, # AT18 prize "Metamorphosis" : self.les_grp, # Seems to be anniversary gift "Shapash" : self.les_grp, # AT19 prize "Cybele" : self.les_grp, # AT19 prize "Sidewinder" : self.les_grp, # AT20 prize "Cobra" : self.les_grp, # AT20 prize "Python" : self.les_grp, # AT20 prize } self.ITEMS_FORCEGROUP_R = self.__makeRevDict(self.ITEMS_FORCEGROUP) for grp, itemNames in self.ITEMS_FORCEGROUP_R.items(): grp.addItems = list(self.getItem(i) for i in itemNames) self.customGroups.add(self.les_grp) # List of items which are forcibly published or hidden self.ITEMS_FORCEPUBLISHED = { "Data Subverter I" : False, # Not used in EVE, probably will appear with Dust link "QA Cross Protocol Analyzer" : False, # QA modules used by CCP internally "QA Damage Module" : False, "QA ECCM" : False, "QA Immunity Module" : False, "QA Multiship Module - 10 Players" : False, "QA Multiship Module - 20 Players" : False, "QA Multiship Module - 40 Players" : False, "QA Multiship Module - 5 Players" : False, "QA Remote Armor Repair System - 5 Players": False, "QA Shield Transporter - 5 Players" : False, "Goru's Shuttle" : False, "Guristas Shuttle" : False, "Mobile Decoy Unit" : False, # Seems to be left over test mod for deployables "Tournament Micro Jump Unit" : False, # Normally seen only on tournament arenas } # do not publish ships that we convert for name in conversions.packs['skinnedShips']: self.ITEMS_FORCEPUBLISHED[name] = False if config.debug: # Publish Tactical Dessy Modes if in debug # Cannot use GROUPS_FORCEPUBLISHED as this does not force items # within group to be published, but rather for the group itself # to show up on ship list group = self.getGroup("Ship Modifiers", eager="items") for item in group.items: self.ITEMS_FORCEPUBLISHED[item.name] = True # List of groups which are forcibly published self.GROUPS_FORCEPUBLISHED = { "Prototype Exploration Ship": False } # We moved the only ship from this group to other group anyway # Dictionary of items with forced meta groups, uses following format: # Item name: (metagroup name, parent type name) self.ITEMS_FORCEDMETAGROUP = { "'Habitat' Miner I": ("Storyline", "Miner I"), "'Wild' Miner I": ("Storyline", "Miner I"), "Khanid Navy Torpedo Launcher": ("Faction", "Torpedo Launcher I"), "Dread Guristas Standup Variable Spectrum ECM": ("Structure Faction", "Standup Variable Spectrum ECM I"), "Dark Blood Standup Heavy Energy Neutralizer": ("Structure Faction", "Standup Heavy Energy Neutralizer I")} # Parent type name: set(item names) self.ITEMS_FORCEDMETAGROUP_R = {} for item, value in list(self.ITEMS_FORCEDMETAGROUP.items()): parent = value[1] if parent not in self.ITEMS_FORCEDMETAGROUP_R: self.ITEMS_FORCEDMETAGROUP_R[parent] = set() self.ITEMS_FORCEDMETAGROUP_R[parent].add(item) # Dictionary of items with forced market group (service assumes they have no # market group assigned in db, otherwise they'll appear in both original and forced groups) self.ITEMS_FORCEDMARKETGROUP = { "Advanced Cerebral Accelerator" : 2487, # Implants & Boosters > Booster > Cerebral Accelerators "Civilian Hobgoblin" : 837, # Drones > Combat Drones > Light Scout Drones "Civilian Light Missile Launcher" : 640, # Ship Equipment > Turrets & Launchers > Missile Launchers > Light Missile Launchers "Civilian Scourge Light Missile" : 920, # Ammunition & Charges > Missiles > Light Missiles > Standard Light Missiles "Civilian Small Remote Armor Repairer" : 1059, # Ship Equipment > Hull & Armor > Remote Armor Repairers > Small "Civilian Small Remote Shield Booster" : 603, # Ship Equipment > Shield > Remote Shield Boosters > Small "Hardwiring - Zainou 'Sharpshooter' ZMX10" : 1493, # Implants & Boosters > Implants > Skill Hardwiring > Missile Implants > Implant Slot 06 "Hardwiring - Zainou 'Sharpshooter' ZMX100" : 1493, # Implants & Boosters > Implants > Skill Hardwiring > Missile Implants > Implant Slot 06 "Hardwiring - Zainou 'Sharpshooter' ZMX1000": 1493, # Implants & Boosters > Implants > Skill Hardwiring > Missile Implants > Implant Slot 06 "Hardwiring - Zainou 'Sharpshooter' ZMX11" : 1493, # Implants & Boosters > Implants > Skill Hardwiring > Missile Implants > Implant Slot 06 "Hardwiring - Zainou 'Sharpshooter' ZMX110" : 1493, # Implants & Boosters > Implants > Skill Hardwiring > Missile Implants > Implant Slot 06 "Hardwiring - Zainou 'Sharpshooter' ZMX1100": 1493, # Implants & Boosters > Implants > Skill Hardwiring > Missile Implants > Implant Slot 06 "Prototype Cerebral Accelerator" : 2487, # Implants & Boosters > Booster > Cerebral Accelerators "Prototype Iris Probe Launcher" : 712, # Ship Equipment > Scanning Equipment > Scan Probe Launchers "Standard Cerebral Accelerator" : 2487, # Implants & Boosters > Booster > Cerebral Accelerators } self.ITEMS_FORCEDMARKETGROUP_R = self.__makeRevDict(self.ITEMS_FORCEDMARKETGROUP) self.FORCEDMARKETGROUP = { 685: False, # Ship Equipment > Electronic Warfare > ECCM 681: False, # Ship Equipment > Electronic Warfare > Sensor Backup Arrays 1639: False, # Ship Equipment > Fleet Assistance > Command Processors 2527: True, # Ship Equipment > Hull & Armor > Mutadaptive Remote Armor Repairers - has hasTypes set to 1 while actually having no types } # Misc definitions # 0 is for items w/o meta group self.META_MAP = OrderedDict([("faction", frozenset((4, 3, 52))), ("complex", frozenset((6,))), ("officer", frozenset((5,)))]) nonNormalMetas = set(chain(*self.META_MAP.values())) self.META_MAP["normal"] = frozenset((0, *(mg.ID for mg in eos.db.getMetaGroups() if mg.ID not in nonNormalMetas))) self.META_MAP.move_to_end("normal", last=False) self.META_MAP_REVERSE = {sv: k for k, v in self.META_MAP.items() for sv in v} self.META_MAP_REVERSE_GROUPED = {} i = 0 for mgids in self.META_MAP.values(): for mgid in mgids: self.META_MAP_REVERSE_GROUPED[mgid] = i i += 1 self.META_MAP_REVERSE_INDICES = self.__makeReverseMetaMapIndices() self.SEARCH_CATEGORIES = ( "Drone", "Module", "Subsystem", "Charge", "Implant", "Deployable", "Fighter", "Structure", "Structure Module", ) self.SEARCH_GROUPS = ( "Ice Product", "Cargo Container", "Secure Cargo Container", "Audit Log Secure Container", "Freight Container", "Jump Filaments", "Triglavian Space Filaments" ) self.ROOT_MARKET_GROUPS = (9, # Ship Equipment 1111, # Rigs 157, # Drones 11, # Ammunition & Charges 1112, # Subsystems 24, # Implants & Boosters 404, # Deployable Structures 2202, # Structure Equipment 2203, # Structure Modifications 2456 # Filaments ) self.SHOWN_MARKET_GROUPS = eos.db.getMarketTreeNodeIds(self.ROOT_MARKET_GROUPS) self.FIT_CATEGORIES = ['Ship'] self.FIT_GROUPS = ['Citadel', 'Engineering Complex', 'Refinery'] # Tell other threads that Market is at their service mktRdy.set() @classmethod def getInstance(cls): if cls.instance is None: cls.instance = Market() return cls.instance @staticmethod def __makeRevDict(orig): """Creates reverse dictionary""" rev = {} for item, value in list(orig.items()): if value not in rev: rev[value] = set() rev[value].add(item) return rev def __makeReverseMetaMapIndices(self): revmap = {} i = 0 for mgids in self.META_MAP.values(): for mgid in mgids: revmap[mgid] = i i += 1 return revmap @staticmethod def getItem(identity, *args, **kwargs): """Get item by its ID or name""" try: if isinstance(identity, types_Item): item = identity elif isinstance(identity, int): item = eos.db.getItem(identity, *args, **kwargs) elif isinstance(identity, str): # We normally lookup with string when we are using import/export # features. Check against overrides identity = conversions.all.get(identity, identity) item = eos.db.getItem(identity, *args, **kwargs) elif isinstance(identity, float): id_ = int(identity) item = eos.db.getItem(id_, *args, **kwargs) else: raise TypeError("Need Item object, integer, float or string as argument") except (KeyboardInterrupt, SystemExit): raise except: pyfalog.error("Could not get item: {0}", identity) raise 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): return identity elif isinstance(identity, (int, float, str)): if isinstance(identity, float): identity = int(identity) # Check custom groups for cgrp in self.customGroups: # During first comparison we need exact int, not float for matching if cgrp.ID == identity or cgrp.name == identity: # Return first match return cgrp # Return eos group if everything else returned nothing return eos.db.getGroup(identity, *args, **kwargs) else: raise TypeError("Need Group object, integer, float or string as argument") @staticmethod def getCategory(identity, *args, **kwargs): """Get category by its ID or name""" if isinstance(identity, types_Category): category = identity elif isinstance(identity, (int, str)): category = eos.db.getCategory(identity, *args, **kwargs) elif isinstance(identity, float): id_ = int(identity) category = eos.db.getCategory(id_, *args, **kwargs) else: raise TypeError("Need Category object, integer, float or string as argument") return category @staticmethod def getMetaGroup(identity, *args, **kwargs): """Get meta group by its ID or name""" if isinstance(identity, types_MetaGroup): metaGroup = identity elif isinstance(identity, (int, str)): metaGroup = eos.db.getMetaGroup(identity, *args, **kwargs) elif isinstance(identity, float): id_ = int(identity) metaGroup = eos.db.getMetaGroup(id_, *args, **kwargs) else: raise TypeError("Need MetaGroup object, integer, float or string as argument") return metaGroup @staticmethod def getMarketGroup(identity, *args, **kwargs): """Get market group by its ID""" if isinstance(identity, types_MarketGroup): marketGroup = identity elif isinstance(identity, (int, float)): id_ = int(identity) marketGroup = eos.db.getMarketGroup(id_, *args, **kwargs) else: raise TypeError("Need MarketGroup object, integer or float as argument") return marketGroup def getGroupByItem(self, item): """Get group by item""" if item.typeName in self.ITEMS_FORCEGROUP: group = self.ITEMS_FORCEGROUP[item.typeName] else: group = item.group return group def getCategoryByItem(self, item): """Get category by item""" grp = self.getGroupByItem(item) cat = grp.category return cat def getMetaGroupByItem(self, item): """Get meta group by item""" # Check if item is in forced metagroup map if item.name in self.ITEMS_FORCEDMETAGROUP: metaGroupName = self.ITEMS_FORCEDMETAGROUP[item.name][0] metaGroup = eos.db.getMetaGroup(metaGroupName) # If no forced meta group is provided, try to use item's # meta group if any else: metaGroup = item.metaGroup return metaGroup def getMetaGroupIdByItem(self, item, fallback=0): """Get meta group ID by item""" id_ = getattr(self.getMetaGroupByItem(item), "ID", fallback) return id_ def getMarketGroupByItem(self, item, parentcheck=True): """Get market group by item, its ID or name""" # Check if we force market group for given item if item.name in self.ITEMS_FORCEDMARKETGROUP: mgid = self.ITEMS_FORCEDMARKETGROUP[item.name] if mgid in self.SHOWN_MARKET_GROUPS: return self.getMarketGroup(mgid) else: return None # Check if item itself has market group elif item.marketGroupID: if item.marketGroupID in self.SHOWN_MARKET_GROUPS: return item.marketGroup else: return None elif parentcheck: # If item doesn't have marketgroup, check if it has parent # item and use its market group parent = self.getParentItemByItem(item, selfparent=False) if parent and parent.marketGroupID in self.SHOWN_MARKET_GROUPS: return parent.marketGroup else: return None else: return None def getParentItemByItem(self, item, selfparent=True): """Get parent item by item""" parent = None if item.name in self.ITEMS_FORCEDMETAGROUP: parentName = self.ITEMS_FORCEDMETAGROUP[item.name][1] parent = self.getItem(parentName) if parent is None: parent = item.varParent # Consider self as parent if item has no parent in database if parent is None and selfparent is True: parent = item return parent def getVariationsByItems(self, items, alreadyparent=False): """Get item variations by item, its ID or name""" # Set for IDs of parent items parents = set() # Set-container for variables variations = set() variations_limiter = set() # if item belongs to these categories, use their group to find "variations" categories = ['Drone', 'Fighter', 'Implant'] for item in items: if item.category.ID == 20 and item.group.ID != 303: # Implants not Boosters implant_remove_list = set() implant_remove_list.add("Low-Grade ") implant_remove_list.add("Low-grade ") implant_remove_list.add("Mid-Grade ") implant_remove_list.add("Mid-grade ") implant_remove_list.add("High-Grade ") implant_remove_list.add("High-grade ") implant_remove_list.add("Limited ") implant_remove_list.add(" - Advanced") implant_remove_list.add(" - Basic") implant_remove_list.add(" - Elite") implant_remove_list.add(" - Improved") implant_remove_list.add(" - Standard") for implant_prefix in ("-6", "-7", "-8", "-9", "-10"): for i in range(50): implant_remove_list.add(implant_prefix + str("%02d" % i)) for text_to_remove in implant_remove_list: if text_to_remove in item.name: variations_limiter.add(item.name.replace(text_to_remove, "")) # Get parent item if alreadyparent is False: parent = self.getParentItemByItem(item) else: parent = item # Combine both in the same set parents.add(parent) # Check for overrides and add them if any if parent.name in self.ITEMS_FORCEDMETAGROUP_R: for _item in self.ITEMS_FORCEDMETAGROUP_R[parent.name]: i = self.getItem(_item) if i: variations.add(i) # Add all parents to variations set variations.update(parents) # Add all variations of parents to the set parentids = tuple(item.ID for item in parents) groupids = tuple(item.group.ID for item in parents if item.category.name in categories) variations_list = eos.db.getVariations(parentids, groupids) if variations_limiter: for limit in variations_limiter: trimmed_variations_list = [variation_item for variation_item in variations_list if limit in variation_item.name] if trimmed_variations_list: variations_list = trimmed_variations_list # If the items are boosters then filter variations to only include boosters for the same slot. BOOSTER_GROUP_ID = 303 if all(map(lambda i: i.group.ID == BOOSTER_GROUP_ID, items)) and len(items) > 0: # 'boosterness' is the database's attribute name for Booster Slot reqSlot = next(items.__iter__()).getAttribute('boosterness') # If the item and it's variation both have a marketGroupID it should match for the variation to be considered valid. marketGroupID = [next(filter(None, map(lambda i: i.marketGroupID, items)), None), None] matchSlotAndMktGrpID = lambda v: v.getAttribute('boosterness') == reqSlot and v.marketGroupID in marketGroupID variations_list = list(filter(matchSlotAndMktGrpID, variations_list)) variations.update(variations_list) return variations def getGroupsByCategory(self, cat): """Get groups from given category""" groups = set([grp for grp in cat.groups if self.getPublicityByGroup(grp)]) return groups @staticmethod def getMarketGroupChildren(mg): """Get the children marketGroups of marketGroup.""" children = set() for child in mg.children: children.add(child) return children def getItemsByGroup(self, group): """Get items assigned to group""" # Return only public items; also, filter out items # which were forcibly set to other groups groupItems = set(group.items) if hasattr(group, 'addItems'): groupItems.update(group.addItems) items = set([ item for item in groupItems if self.getPublicityByItem(item) and self.getGroupByItem(item) == group]) return items def getItemsByMarketGroup(self, mg, vars_=True): """Get items in the given market group""" result = set() # Get items from eos market group baseitms = set(mg.items) # Add hardcoded items to set if mg.ID in self.ITEMS_FORCEDMARKETGROUP_R: forceditms = set(self.getItem(itmn) for itmn in self.ITEMS_FORCEDMARKETGROUP_R[mg.ID]) baseitms.update(forceditms) if vars_: parents = set() for item in baseitms: # Add one of the base market group items to result result.add(item) parent = self.getParentItemByItem(item, selfparent=False) # If item has no parent, it's base item (or at least should be) if parent is None: parents.add(item) # Fetch variations only for parent items variations = self.getVariationsByItems(parents, alreadyparent=True) for variation in variations: # Exclude items with their own explicitly defined market groups if self.getMarketGroupByItem(variation, parentcheck=False) is None: result.add(variation) else: result = baseitms # Get rid of unpublished items result = set([item_ for item_ in result if self.getPublicityByItem(item_)]) return result def marketGroupHasTypesCheck(self, mg): """If market group has any items, return true""" if mg and mg.ID in self.ITEMS_FORCEDMARKETGROUP_R: # This shouldn't occur normally but makes errors more mild when ITEMS_FORCEDMARKETGROUP is outdated. if len(mg.children) > 0 and len(mg.items) == 0: pyfalog.error(("Market group \"{0}\" contains no items and has children. " "ITEMS_FORCEDMARKETGROUP is likely outdated and will need to be " "updated for {1} to display correctly.").format(mg, self.ITEMS_FORCEDMARKETGROUP_R[mg.ID])) return False return True elif len(mg.items) > 0 and len(mg.children) == 0: return True else: return False def marketGroupValidityCheck(self, mg): """Check market group validity""" # The only known case when group can be invalid is # when it's declared to have types, but it doesn't contain anything if mg.ID in self.FORCEDMARKETGROUP: return self.FORCEDMARKETGROUP[mg.ID] if mg.hasTypes and not self.marketGroupHasTypesCheck(mg): return False else: return True def getIconByMarketGroup(self, mg): """Return icon associated to marketgroup""" if mg.iconID: return mg.iconID else: while mg and not mg.hasTypes: mg = mg.parent if not mg: return "" elif self.marketGroupHasTypesCheck(mg): # Do not request variations to make process faster # Pick random item and use its icon items = self.getItemsByMarketGroup(mg, vars_=False) try: item = items.pop() except KeyError: return "" return item.iconID if getattr(item, "icon", None) else "" elif self.getMarketGroupChildren(mg) > 0: kids = self.getMarketGroupChildren(mg) mktGroups = self.getIconByMarketGroup(kids) size = len(mktGroups) return mktGroups.pop() if size > 0 else "" else: return "" def getPublicityByItem(self, item): """Return if an item is published""" if item.typeName in self.ITEMS_FORCEPUBLISHED: pub = self.ITEMS_FORCEPUBLISHED[item.typeName] else: pub = item.published return pub def getPublicityByGroup(self, group): """Return if an group is published""" if group.name in self.GROUPS_FORCEPUBLISHED: pub = self.GROUPS_FORCEPUBLISHED[group.name] else: pub = group.published return pub def getMarketRoot(self): """ Get the root of the market tree. Returns a list, where each element is a tuple containing: the ID, the name and the icon of the group """ root = set() for id_ in self.ROOT_MARKET_GROUPS: mg = self.getMarketGroup(id_) root.add(mg) return root def getShipRoot(self): cat1 = self.getCategory("Ship") cat2 = self.getCategory("Structure") root = set(self.getGroupsByCategory(cat1) | self.getGroupsByCategory(cat2)) return root def getShipList(self, grpid): """Get ships for given group id""" grp = self.getGroup(grpid, eager=("items", "items.group", "items.marketGroup")) ships = self.getItemsByGroup(grp) return ships def getShipListDelayed(self, id_, callback): """Background version of getShipList""" self.shipBrowserWorkerThread.queue.put((id_, callback)) def searchShips(self, name): """Find ships according to given text pattern""" filter_ = types_Category.name.in_(["Ship", "Structure"]) results = eos.db.searchItems(name, where=filter_, join=(types_Item.group, types_Group.category), eager=("group.category", "metaGroup")) ships = set() for item in results: if self.getPublicityByItem(item): ships.add(item) return ships def searchItems(self, name, callback, filterName=None): """Find items according to given text pattern""" self.searchWorkerThread.scheduleSearch(name, callback, filterName) @staticmethod def getItemsWithOverrides(): overrides = eos.db.getAllOverrides() items = set() for x in overrides: if x.item is None: eos.db.saveddata_session.delete(x) eos.db.commit() else: items.add(x.item) return list(items) @staticmethod def directAttrRequest(items, attribs): try: itemIDs = tuple([i.ID for i in items]) except TypeError: itemIDs = (items.ID,) try: attrIDs = tuple([i.ID for i in attribs]) except TypeError: attrIDs = (attribs.ID,) info = {} for itemID, typeID, val in eos.db.directAttributeRequest(itemIDs, attrIDs): info[itemID] = val return info def getImplantTree(self): """Return implant market group children""" img = self.getMarketGroup(27) return self.getMarketGroupChildren(img) def filterItemsByMeta(self, items, metas): """Filter items by meta lvl""" filtered = [item for item in items if self.getMetaGroupIdByItem(item) in metas] return filtered def getReplacements(self, identity): item = self.getItem(identity) # We already store needed type IDs in database replTypeIDs = {int(i) for i in item.replacements.split(",") if i} if item.replacements is not None else {} if not replTypeIDs: return () # As replacements were generated without keeping track which items were published, # filter them out here items = [] for typeID in replTypeIDs: item = self.getItem(typeID) if not item: continue if self.getPublicityByItem(item): items.append(item) return items def getRecentlyUsed(self): recentlyUsedItems = [] for itemID in self.serviceMarketRecentlyUsedModules["pyfaMarketRecentlyUsedModules"]: item = self.getItem(itemID) if item is None: self.serviceMarketRecentlyUsedModules["pyfaMarketRecentlyUsedModules"].remove(itemID) recentlyUsedItems.append(item) return recentlyUsedItems def storeRecentlyUsed(self, itemID): recentlyUsedModules = self.serviceMarketRecentlyUsedModules["pyfaMarketRecentlyUsedModules"] while itemID in recentlyUsedModules: recentlyUsedModules.remove(itemID) item = self.getItem(itemID) if item.isAbyssal: return while len(recentlyUsedModules) >= 20: recentlyUsedModules.pop(-1) recentlyUsedModules.insert(0, itemID) def itemSort(self, item, reverseMktGrp=False): catname = self.getCategoryByItem(item).name try: mktgrpid = self.getMarketGroupByItem(item).ID except AttributeError: mktgrpid = -1 pyfalog.warning("unable to find market group for {}".format(item.typeName)) if reverseMktGrp: mktgrpid = -mktgrpid parentname = self.getParentItemByItem(item).name # Get position of market group metagrpid = self.getMetaGroupIdByItem(item) metatab = self.META_MAP_REVERSE_GROUPED.get(metagrpid) metalvl = item.metaLevel or 0 return catname, mktgrpid, parentname, metatab, metalvl, item.name def printAllItems(self): items = set() def handleMg(marketGroup, path=()): marketGroup = self.getMarketGroup(marketGroup, eager=("items", "items.metaGroup", "children")) path = path + (marketGroup.name,) print(' > '.join(path)) for item in self.getItemsByMarketGroup(marketGroup): items.add(item.ID) for mgc in self.getMarketGroupChildren(marketGroup): handleMg(mgc, path=path) for mg in self.ROOT_MARKET_GROUPS: handleMg(mg) print(sorted(items))