NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

skillsvc.py (48619B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\services\skillsvc.py
      2 import operator
      3 from carbon.common.script.sys.service import Service
      4 from collections import defaultdict, OrderedDict
      5 from copy import deepcopy
      6 import blue
      7 import telemetry
      8 import carbonui.const as uiconst
      9 import characterskills
     10 import clonegrade
     11 import eve.common.script.util.notificationconst as notificationConst
     12 import evetypes
     13 import localization
     14 import uthread
     15 import utillib
     16 from carbon.client.script.environment.AudioUtil import PlaySound
     17 from characterskills.client import skill_signals
     18 from characterskills.client.skill_training import ClientCharacterSkillInterface
     19 from characterskills.common.character_skill_entry import CharacterSkillEntry
     20 from characterskills.skill_accelerators import SkillAcceleratorBoosters
     21 from characterskills.skill_training import SkillTrainingTimeCalculator
     22 from characterskills.util import GetSkillPointsPerMinute, GetSPForLevelRaw, GetSkillLevelRaw, GetAllSkillGroups
     23 from dogma.const import attributePrimaryAttribute, attributeSkillTimeConstant, attributeSecondaryAttribute
     24 from dogma.effects import IsBoosterSkillAccelerator
     25 from eve.common.script.sys import eveCfg
     26 from omega.client.clone_grade_svc import get_clone_grade_svc
     27 from player_messaging.client.ui_message import prompt_player_fsd_dialog
     28 from shipprogression.shipUnlockSvc import GetShipUnlockService
     29 from skills.client.skillController import SkillController
     30 from eve.client.script.ui.skilltrading.skillExtractorWindow import SkillExtractorWindow
     31 from eve.common.lib import appConst as const
     32 from eveexceptions import UserError
     33 from evetypes.skills import get_required_skills_index, get_dogma_required_skills
     34 from inventorycommon import const as invconst
     35 from notifications.common.formatters.skillPoints import UnusedSkillPointsFormatter
     36 from notifications.common.notification import Notification, SimpleNotification
     37 from eve.common.script.util import notificationconst
     38 SKILLREQ_DONTHAVE = 1
     39 SKILLREQ_HAVEBUTNOTTRAINED = 2
     40 SKILLREQ_HAVEANDTRAINED = 3
     41 SKILLREQ_HAVEANDTRAINEDFULLY = 4
     42 SKILLREQ_TRIALRESTRICTED = 5
     43 TEXTURE_PATH_BY_SKILLREQ = {SKILLREQ_DONTHAVE: 'res:/UI/Texture/Classes/Skills/doNotHaveFrame.png',
     44  SKILLREQ_HAVEBUTNOTTRAINED: 'res:/UI/Texture/Classes/Skills/levelPartiallyTrainedFrame.png',
     45  SKILLREQ_HAVEANDTRAINED: 'res:/UI/Texture/Classes/Skills/levelTrainedFrame.png',
     46  SKILLREQ_HAVEANDTRAINEDFULLY: 'res:/UI/Texture/Classes/Skills/fullyTrainedFrame.png',
     47  SKILLREQ_TRIALRESTRICTED: 'res:/UI/Texture/Classes/Skills/trialRestrictedFrame.png'}
     48 SHIP_SKILLREQ_HINT = {SKILLREQ_DONTHAVE: 'UI/InfoWindow/ShipSkillReqDoNotHave',
     49  SKILLREQ_HAVEBUTNOTTRAINED: 'UI/InfoWindow/ShipSkillReqPartiallyTrained',
     50  SKILLREQ_HAVEANDTRAINED: 'UI/InfoWindow/ShipSkillReqTrained',
     51  SKILLREQ_HAVEANDTRAINEDFULLY: 'UI/InfoWindow/ShipSkillReqFullyTrained',
     52  SKILLREQ_TRIALRESTRICTED: 'UI/InfoWindow/ShipSkillReqRestrictedForTrial'}
     53 SKILL_SKILLREQ_HINT = {SKILLREQ_DONTHAVE: 'UI/InfoWindow/SkillReqDoNotHave',
     54  SKILLREQ_HAVEBUTNOTTRAINED: 'UI/InfoWindow/SkillReqPartiallyTrained',
     55  SKILLREQ_HAVEANDTRAINED: 'UI/InfoWindow/SkillReqTrained',
     56  SKILLREQ_HAVEANDTRAINEDFULLY: 'UI/InfoWindow/SkillReqFullyTrained',
     57  SKILLREQ_TRIALRESTRICTED: 'UI/InfoWindow/SkillReqRestrictedForTrial'}
     58 ITEM_SKILLREQ_HINT = {SKILLREQ_DONTHAVE: 'UI/InfoWindow/ItemSkillReqDoNotHave',
     59  SKILLREQ_HAVEBUTNOTTRAINED: 'UI/InfoWindow/ItemSkillReqPartiallyTrained',
     60  SKILLREQ_HAVEANDTRAINED: 'UI/InfoWindow/ItemSkillReqTrained',
     61  SKILLREQ_HAVEANDTRAINEDFULLY: 'UI/InfoWindow/ItemSkillReqFullyTrained',
     62  SKILLREQ_TRIALRESTRICTED: 'UI/InfoWindow/ItemSkillReqRestrictedForTrial'}
     63 HIGHLIGHT_SKILLS_TRAINED = 2243
     64 HIGHLIGHT_SP_ADDED = 2244
     65 HIGHLIGHT_SKILLS_TRAINED_AND_SP_ADDED = 2259
     66 
     67 class SkillsSvc(Service):
     68     __guid__ = 'svc.skills'
     69     __notifyevents__ = ['ProcessSessionChange',
     70      'OnServerSkillsChanged',
     71      'OnServerSkillsRemoved',
     72      'OnSkillForcedRefresh',
     73      'OnRespecInfoChanged',
     74      'OnFreeSkillPointsChanged',
     75      'OnServerBoostersChanged',
     76      'OnServerImplantsChanged',
     77      'OnCloneDestruction',
     78      'OnLogonSkillsTrained',
     79      'OnAlphaInjectionAvailable',
     80      'OnSkillBundleInjected',
     81      'OnSessionReset']
     82     __servicename__ = 'skills'
     83     __displayname__ = 'Skill Client Service'
     84     __dependencies__ = ['nonDiminishingInjection', 'settings']
     85     __startupdependencies__ = ['godma']
     86 
     87     def Run(self, memStream = None):
     88         self.LogInfo('Starting Skills')
     89         self.Reset()
     90         GetShipUnlockService(self)
     91 
     92     def Stop(self, memStream = None):
     93         self.Reset()
     94 
     95     def ProcessSessionChange(self, isremote, session, change):
     96         if session.charid is None:
     97             self.Reset()
     98 
     99     def OnSessionReset(self):
    100         self.Reset()
    101         self.ResetSkillHistory()
    102 
    103     def Reset(self):
    104         self.allskills = None
    105         self.skillGroups = None
    106         self.respecInfo = None
    107         self.depedentSkills = None
    108         self.myskills = None
    109         self.mySkillsIncludingLapsed = None
    110         self.skillHistory = None
    111         self.freeSkillPoints = None
    112         self.skillHandler = None
    113         self.boosters = None
    114         self.implants = None
    115         self.characterAttributes = None
    116         self._skillAcceleratorBoosters = None
    117         self.skillsByGroupIDTypeID = None
    118         self._charTotalSkillPoints = None
    119 
    120     def ResetSkillHistory(self):
    121         self.skillHistory = None
    122 
    123     def GetSkillHandler(self):
    124         if not self.skillHandler:
    125             self.skillHandler = session.ConnectToRemoteService('skillMgr2').GetMySkillHandler()
    126         return self.skillHandler
    127 
    128     def RefreshMySkills(self):
    129         if self.myskills is not None:
    130             self.LogError('skillSvc is force refreshing client side skill state!')
    131         self.myskills = self.GetSkillHandler().GetSkills()
    132 
    133     def RefreshMySkillsIncludingLapsed(self):
    134         if self.mySkillsIncludingLapsed is not None:
    135             self.LogError('skillSvc is force refreshing client side skill state!')
    136         self.mySkillsIncludingLapsed = self.GetSkillHandler().GetAllSkills()
    137 
    138     def OnLogonSkillsTrained(self, skillInfos, canTrain):
    139         levelsTrained = []
    140         for skillTypeID, skillInfo in skillInfos.iteritems():
    141             for x in xrange(skillInfo.currentLevel + 1, skillInfo.skillLevel + 1):
    142                 levelsTrained.append((skillTypeID, x))
    143 
    144         if levelsTrained:
    145             PlaySound('msg_SkillTrained_play')
    146             self.TriggerSkillsTrainedNotification(levelsTrained)
    147         if canTrain:
    148             self._CheckTriggerNoSkillsInQueueNotification()
    149         GetShipUnlockService().OnSkillsChanged(skillInfos)
    150 
    151     def OnServerSkillsChanged(self, skillInfos, event, timeStamp):
    152         levelsTrained = self._UpdateMySkillsAndReturnLevelsTrained(skillInfos, timeStamp)
    153         self.ResetSkillHistory()
    154         sm.GetService('skillqueue').OnServerSkillsChanged(skillInfos)
    155         if levelsTrained:
    156             PlaySound('msg_SkillTrained_play')
    157             self.TriggerSkillsTrainedNotification(levelsTrained)
    158             sm.ScatterEvent('OnSkillLevelsTrained', levelsTrained)
    159             skill_signals.on_skill_levels_trained(levelsTrained)
    160         if event:
    161             sm.ScatterEvent(event, skillInfos)
    162         sm.ScatterEvent('OnSkillsChanged', skillInfos)
    163 
    164     def OnServerSkillsRemoved(self, skillInfos, timeStamp):
    165         self._UpdateMySkillsAndReturnLevelsTrained(skillInfos, timeStamp)
    166         self.ResetSkillHistory()
    167         sm.GetService('skillqueue').OnServerSkillsChanged(skillInfos)
    168         sm.ScatterEvent('OnSkillsChanged', skillInfos)
    169 
    170     def _UpdateMySkillsAndReturnLevelsTrained(self, skillInfos, timeStamp):
    171         levelsTrained = []
    172         self.GetSkills()
    173         skills = self.GetSkillsIncludingLapsed()
    174         for skillTypeID, skillInfo in skillInfos.iteritems():
    175             if skillInfo.trainedSkillPoints >= 0:
    176                 currentSkill = skills.get(skillTypeID, None)
    177                 if currentSkill and getattr(currentSkill, 'timeStamp', 0) > timeStamp:
    178                     continue
    179                 if not currentSkill or currentSkill.trainedSkillLevel != skillInfo.trainedSkillLevel:
    180                     if currentSkill and currentSkill.trainedSkillLevel:
    181                         startLevel = currentSkill.trainedSkillLevel + 1
    182                     else:
    183                         startLevel = 1
    184                     endLevel = skillInfo.trainedSkillLevel + 1
    185                     for x in xrange(startLevel, endLevel):
    186                         levelsTrained.append((skillTypeID, x))
    187 
    188                 skillInfo.timeStamp = timeStamp
    189                 self.myskills[skillTypeID] = skillInfo
    190                 self.mySkillsIncludingLapsed[skillTypeID] = skillInfo
    191             elif skillInfo.virtualSkillLevel is not None:
    192                 currentSkill = skills.get(skillTypeID, None)
    193                 if currentSkill and getattr(currentSkill, 'timeStamp', 0) > timeStamp:
    194                     continue
    195                 skillInfo = skillInfo.GetCopyWithNewSkillPoints(None)
    196                 skillInfo.timeStamp = timeStamp
    197                 self.myskills[skillTypeID] = skillInfo
    198                 self.mySkillsIncludingLapsed[skillTypeID] = skillInfo
    199             else:
    200                 del self.myskills[skillTypeID]
    201                 del self.mySkillsIncludingLapsed[skillTypeID]
    202 
    203         return levelsTrained
    204 
    205     def GetSkills(self, renew = 0):
    206         if self.myskills is None or renew:
    207             self.RefreshMySkills()
    208         return self.myskills
    209 
    210     def GetCharacterAttributes(self, renew = False):
    211         if self.characterAttributes is None or renew:
    212             self.GetBoosters(True)
    213             self.GetImplants(True)
    214             self.characterAttributes = self.GetSkillHandler().GetAttributes()
    215             for attributeID, attributeValue in self.characterAttributes.iteritems():
    216                 self.godma.GetStateManager().ApplyAttributeChange(session.charid, session.charid, attributeID, blue.os.GetWallclockTime(), attributeValue, None, False)
    217 
    218         return deepcopy(self.characterAttributes)
    219 
    220     def GetCharacterAttribute(self, attributeID, includeBoosters = True):
    221         if get_clone_grade_svc().is_omega_with_grace_period():
    222             attrValue = self.GetCharacterAttributes()[attributeID]
    223         else:
    224             attrValue = self.GetCharacterAttributes()[attributeID] * clonegrade.ALPHA_TRAINING_MULTIPLIER
    225         if not includeBoosters:
    226             boosters = self.GetSkillAcceleratorBoosters()
    227             bonus = boosters.get_attribute_bonus(attributeID)
    228             if not get_clone_grade_svc().is_omega_with_grace_period():
    229                 bonus = bonus * clonegrade.ALPHA_TRAINING_MULTIPLIER
    230             attrValue -= bonus
    231         return attrValue
    232 
    233     def GetSkill(self, skillTypeID, renew = 0):
    234         if self.myskills is None or renew:
    235             self.RefreshMySkills()
    236         return self.myskills.get(skillTypeID, None)
    237 
    238     def GetSkillIncludingLapsed(self, skillTypeID, renew = 0):
    239         if self.mySkillsIncludingLapsed is None or renew:
    240             self.RefreshMySkillsIncludingLapsed()
    241         return self.mySkillsIncludingLapsed.get(skillTypeID, None)
    242 
    243     def GetSkillsIncludingLapsed(self, renew = 0):
    244         if self.mySkillsIncludingLapsed is None or renew:
    245             self.RefreshMySkillsIncludingLapsed()
    246         return self.mySkillsIncludingLapsed
    247 
    248     @telemetry.ZONE_METHOD
    249     def MySkillLevel(self, skillTypeID):
    250         skill = self.GetSkill(skillTypeID)
    251         if skill is not None:
    252             return skill.trainedSkillLevel
    253         return 0
    254 
    255     def MySkillLevelIncludingQueued(self, skillID):
    256         inQueue = self.GetMaxSkillLevelsInQueue()
    257         skill = self.GetSkillIncludingLapsed(skillID)
    258         if skill is None:
    259             return 0
    260         return max(skill.trainedSkillLevel, inQueue.get(skillID, 0))
    261 
    262     def MySkillPoints(self, skillTypeID):
    263         skill = self.GetSkill(skillTypeID)
    264         if skill is not None:
    265             return skill.trainedSkillPoints
    266 
    267     def MyEffectiveSkillLevelsByID(self, renew = 0):
    268         skills = {}
    269         for skillTypeID, skill in self.GetSkills(renew).iteritems():
    270             skills[skillTypeID] = skill.effectiveSkillLevel
    271 
    272         return skills
    273 
    274     def SkillPointsCurrentLevel(self, skillTypeID):
    275         skill = self.GetSkillIncludingLapsed(skillTypeID)
    276         return GetSPForLevelRaw(skill.skillRank, skill.trainedSkillLevel)
    277 
    278     def SkillpointsNextLevel(self, skillTypeID):
    279         skill = self.GetSkillIncludingLapsed(skillTypeID)
    280         if not skill or skill.trainedSkillLevel >= characterskills.MAX_SKILL_LEVEL:
    281             return None
    282         return GetSPForLevelRaw(skill.skillRank, skill.trainedSkillLevel + 1)
    283 
    284     def HasSkill(self, skillTypeID):
    285         return skillTypeID in self.GetSkillsIncludingLapsed()
    286 
    287     def IsSkillInjected(self, skillTypeID):
    288         skill = self.GetSkillIncludingLapsed(skillTypeID)
    289         return skill and skill.trainedSkillLevel is not None
    290 
    291     @telemetry.ZONE_METHOD
    292     def GetAllSkills(self):
    293         if not self.allskills:
    294             self.allskills = {}
    295             for typeID in evetypes.GetTypeIDsByCategory(const.categorySkill):
    296                 if evetypes.IsPublished(typeID):
    297                     self.allskills[typeID] = CharacterSkillEntry(typeID, 0, 0, self.GetSkillRank(typeID), None)
    298 
    299         return self.allskills
    300 
    301     def GetEffectiveLevel(self, typeID):
    302         skills = self.GetSkills()
    303         if typeID in skills:
    304             return skills[typeID].effectiveSkillLevel
    305 
    306     def GetMyLevel(self, typeID):
    307         skills = self.GetSkills()
    308         if typeID in skills:
    309             return skills[typeID].trainedSkillLevel
    310 
    311     def GetMyLevelIncludingLapsed(self, typeID):
    312         skills = self.GetSkillsIncludingLapsed()
    313         if typeID in skills:
    314             return skills[typeID].trainedSkillLevel
    315 
    316     def GetMySkillPoints(self, typeID):
    317         skills = self.GetSkills()
    318         if typeID in skills:
    319             return skills[typeID].trainedSkillPoints
    320         else:
    321             return 0
    322 
    323     def GetMySkillPointsIncludingLapsed(self, typeID):
    324         skills = self.GetSkillsIncludingLapsed()
    325         if typeID in skills:
    326             return skills[typeID].trainedSkillPoints
    327         else:
    328             return 0
    329 
    330     @telemetry.ZONE_METHOD
    331     def GetAllSkillGroups(self):
    332         if not self.skillGroups:
    333             skillGroupIDs = GetAllSkillGroups()
    334             skillgroups = [ utillib.KeyVal(groupID=groupID, groupName=evetypes.GetGroupNameByGroup(groupID)) for groupID in skillGroupIDs ]
    335             skillgroups = localization.util.Sort(skillgroups, key=operator.attrgetter('groupName'))
    336             self.skillGroups = skillgroups
    337         return self.skillGroups
    338 
    339     @telemetry.ZONE_METHOD
    340     def GetSkillHistory(self, maxresults = 50):
    341         if self.skillHistory is None:
    342             self.skillHistory = self.GetSkillHandler().GetSkillHistory(maxresults)
    343         return self.skillHistory
    344 
    345     def GetDependentSkills(self, typeID):
    346         if self.depedentSkills is None:
    347             self.depedentSkills = defaultdict(dict)
    348             for skillTypeID in self.GetAllSkills():
    349                 requirements = self.GetRequiredSkills(skillTypeID)
    350                 for dependentTypeID, level in requirements.iteritems():
    351                     self.depedentSkills[dependentTypeID][skillTypeID] = level
    352 
    353         return self.depedentSkills[typeID]
    354 
    355     @telemetry.ZONE_METHOD
    356     def GetRecentlyTrainedSkills(self):
    357         skillChanges = {}
    358         skillData = self.GetSkillHandler().GetSkillChangesForISIS()
    359         for typeID, pointChange in skillData:
    360             currentSkillPoints = self.MySkillPoints(typeID) or 0
    361             timeConstant = self.godma.GetTypeAttribute2(typeID, const.attributeSkillTimeConstant)
    362             pointsBefore = currentSkillPoints - pointChange
    363             oldLevel = GetSkillLevelRaw(pointsBefore, timeConstant)
    364             if self.MySkillLevel(typeID) > oldLevel:
    365                 skillChanges[typeID] = oldLevel
    366 
    367         return skillChanges
    368 
    369     @telemetry.ZONE_METHOD
    370     def GetSkillGroups(self, advanced = False):
    371         if session.charid:
    372             ownSkills = self.GetSkills()
    373             skillQueue = sm.GetService('skillqueue').GetServerQueue()
    374             skillsInQueue = [ skill.trainingTypeID for skill in skillQueue ]
    375         else:
    376             ownSkills = []
    377             skillsInQueue = []
    378         ownSkillTypeIDs = []
    379         ownSkillsByGroupID = defaultdict(list)
    380         ownSkillsInTrainingByGroupID = defaultdict(list)
    381         ownSkillsInQueueByGroupID = defaultdict(list)
    382         ownSkillPointsByGroupID = defaultdict(int)
    383         for skillTypeID, skill in ownSkills.iteritems():
    384             groupID = evetypes.GetGroupID(skillTypeID)
    385             ownSkillsByGroupID[groupID].append(skill)
    386             if sm.GetService('skillqueue').SkillInTraining(skillTypeID):
    387                 ownSkillsInTrainingByGroupID[groupID].append(skill)
    388             if skillTypeID in skillsInQueue:
    389                 ownSkillsInQueueByGroupID[groupID].append(skillTypeID)
    390             ownSkillPointsByGroupID[groupID] += skill.skillPoints
    391             ownSkillTypeIDs.append(skillTypeID)
    392 
    393         missingSkillsByGroupID = defaultdict(list)
    394         if advanced:
    395             allSkills = self.GetAllSkills()
    396             for skillTypeID, skill in allSkills.iteritems():
    397                 if skillTypeID not in ownSkillTypeIDs:
    398                     groupID = evetypes.GetGroupID(skillTypeID)
    399                     missingSkillsByGroupID[groupID].append(skill)
    400 
    401         skillsByGroup = []
    402         skillgroups = self.GetAllSkillGroups()
    403         for invGroup in skillgroups:
    404             mySkillsInGroup = ownSkillsByGroupID[invGroup.groupID]
    405             skillsIDontHave = missingSkillsByGroupID[invGroup.groupID]
    406             mySkillsInTraining = ownSkillsInTrainingByGroupID[invGroup.groupID]
    407             mySkillsInQueue = ownSkillsInQueueByGroupID[invGroup.groupID]
    408             skillPointsInGroup = ownSkillPointsByGroupID[invGroup.groupID]
    409             skillsByGroup.append([invGroup,
    410              mySkillsInGroup,
    411              skillsIDontHave,
    412              mySkillsInTraining,
    413              mySkillsInQueue,
    414              skillPointsInGroup])
    415 
    416         return skillsByGroup
    417 
    418     def IsSkillRequirementMet(self, typeID):
    419         required = self.GetRequiredSkills(typeID)
    420         for skillTypeID, lvl in required.iteritems():
    421             if self.MySkillLevel(skillTypeID) < lvl:
    422                 return False
    423 
    424         return True
    425 
    426     def IsSkillRequirementMetIncludingSkillQueue(self, typeID):
    427         required = self.GetRequiredSkills(typeID)
    428         inQueue = self.GetMaxSkillLevelsInQueue()
    429         for skillTypeID, lvl in required.iteritems():
    430             if self.MySkillLevel(skillTypeID) < lvl:
    431                 if skillTypeID not in inQueue or inQueue[skillTypeID] < lvl:
    432                     return False
    433 
    434         return True
    435 
    436     @telemetry.ZONE_METHOD
    437     def GetMaxSkillLevelsInQueue(self, maxQueueIdx = None):
    438         skillQueue = sm.GetService('skillqueue').GetQueue()
    439         if maxQueueIdx is not None:
    440             skillQueue = skillQueue[:maxQueueIdx + 1]
    441         inQueue = {}
    442         for skill in skillQueue:
    443             inQueue[skill.trainingTypeID] = skill.trainingToLevel
    444 
    445         return inQueue
    446 
    447     def GetSkillInQueueIndex(self, skillID, level):
    448         for i, skill in enumerate(sm.GetService('skillqueue').GetQueue()):
    449             if skill.trainingTypeID == skillID and skill.trainingToLevel == level:
    450                 return i
    451 
    452     @telemetry.ZONE_METHOD
    453     def GetRequiredSkills(self, typeID):
    454         return get_dogma_required_skills(typeID)
    455 
    456     def GetRequiredSkillsLevel(self, skills):
    457         if not skills:
    458             return SKILLREQ_HAVEANDTRAINED
    459         allLevel5 = True
    460         haveAll = True
    461         missingSkill = False
    462         for skillTypeID, level in skills:
    463             mySkill = self.GetSkill(skillTypeID)
    464             if mySkill is None:
    465                 missingSkill = True
    466                 continue
    467             if mySkill.trainedSkillLevel < level:
    468                 haveAll = False
    469             if mySkill.trainedSkillLevel != 5:
    470                 allLevel5 = False
    471 
    472         if missingSkill:
    473             return SKILLREQ_DONTHAVE
    474         elif not haveAll:
    475             return SKILLREQ_HAVEBUTNOTTRAINED
    476         elif allLevel5:
    477             return SKILLREQ_HAVEANDTRAINEDFULLY
    478         else:
    479             return SKILLREQ_HAVEANDTRAINED
    480 
    481     def IsUnlockedWithExpertSystem(self, typeID):
    482         skills = self.GetRequiredSkills(typeID)
    483         isUnlockedWithExpertSystem = False
    484         for skillTypeID, requiredLevel in skills.iteritems():
    485             mySkill = self.GetSkill(skillTypeID)
    486             if mySkill:
    487                 trainedSkillLevel = mySkill.trainedSkillLevel or 0
    488             else:
    489                 return False
    490             if trainedSkillLevel < requiredLevel and mySkill.effectiveSkillLevel >= requiredLevel:
    491                 isUnlockedWithExpertSystem = True
    492             elif trainedSkillLevel < requiredLevel:
    493                 return False
    494 
    495         return isUnlockedWithExpertSystem
    496 
    497     def GetRequiredSkillsLevelTexturePathAndHint(self, skills, typeID = None):
    498         skillLevel = self.GetRequiredSkillsLevel(skills)
    499         texturePath = TEXTURE_PATH_BY_SKILLREQ[skillLevel]
    500         if typeID is None:
    501             hint = ITEM_SKILLREQ_HINT[skillLevel]
    502         else:
    503             categoryID = evetypes.GetCategoryID(typeID)
    504             if categoryID == invconst.categoryShip:
    505                 hint = SHIP_SKILLREQ_HINT[skillLevel]
    506             elif categoryID == invconst.categorySkill:
    507                 hint = SKILL_SKILLREQ_HINT[skillLevel]
    508             else:
    509                 hint = ITEM_SKILLREQ_HINT[skillLevel]
    510         return (texturePath, localization.GetByLabel(hint))
    511 
    512     def GetRequiredSkillsRecursive(self, typeID):
    513         ret = {}
    514         self._GetAllSkillsRequiredToUseTypeRecursive(typeID, ret)
    515         return ret
    516 
    517     def _GetAllSkillsRequiredToUseTypeRecursive(self, typeID, ret):
    518         for skillTypeID, lvl in self.GetRequiredSkills(typeID).iteritems():
    519             ret[skillTypeID] = max(ret.get(skillTypeID, 0), lvl)
    520             if skillTypeID != typeID:
    521                 self._GetAllSkillsRequiredToUseTypeRecursive(skillTypeID, ret)
    522 
    523     def GetTopLevelSkillsMissingToUseItem(self, typeID):
    524         requiredSkills = self.GetRequiredSkills(typeID)
    525         ret = []
    526         for typeID, level in requiredSkills.iteritems():
    527             if self.GetMyLevel(typeID) < level:
    528                 ret.append((typeID, level))
    529 
    530         return ret
    531 
    532     def GetTotalMissingSkillLevelsToUseItem(self, typeID):
    533         requiredSkills = self.GetRequiredSkillsRecursive(typeID)
    534         missingLevels = 0
    535         for typeID, level in requiredSkills.iteritems():
    536             myLevel = self.GetMyLevel(typeID)
    537             if myLevel is None:
    538                 myLevel = 0
    539             if myLevel < level:
    540                 missingLevels += level - myLevel
    541 
    542         return missingLevels
    543 
    544     def GetMissingSkillBooksFromList(self, skills):
    545         missingSkills = []
    546         for skillID, level in skills:
    547             skill = self.GetSkill(skillID)
    548             if not skill or skill.trainedSkillPoints is None:
    549                 missingSkills.append(skillID)
    550 
    551         return missingSkills
    552 
    553     def GetSkillsMissingToUseItemRecursiveList(self, typeID):
    554         missingDict = self.GetSkillsMissingToUseItemRecursive(typeID)
    555         return [ (typeID, level) for typeID, level in missingDict.iteritems() ]
    556 
    557     def GetSkillsMissingToUseItemRecursive(self, typeID):
    558         ret = OrderedDict()
    559         self._GetMissingSkillsRequiredToUseTypeRecursive(typeID, ret)
    560         return ret
    561 
    562     def _GetMissingSkillsRequiredToUseTypeRecursive(self, typeID, ret):
    563         for skillTypeID, lvl in self.GetRequiredSkills(typeID).iteritems():
    564             if self.GetMyLevel(skillTypeID) < lvl:
    565                 if skillTypeID != typeID:
    566                     mySkill = self.GetSkillIncludingLapsed(skillTypeID)
    567                     if not mySkill or mySkill.trainedSkillPoints <= 0:
    568                         self._GetMissingSkillsRequiredToUseTypeRecursive(skillTypeID, ret)
    569                 ret[skillTypeID] = max(ret.get(skillTypeID, 0), lvl)
    570 
    571     def GetSkillBooksMissingFromList(self, skills):
    572         return [ typeID for typeID in skills.iterkeys() if self.GetSkillIncludingLapsed(typeID) is None ]
    573 
    574     def GetSkillsMissingToUseAllSkillsFromListRecursiveAsList(self, skills):
    575         missingDict = self.GetSkillsMissingToUseAllSkillsFromListRecursive(skills)
    576         return [ (typeID, level) for typeID, level in missingDict.iteritems() ]
    577 
    578     def GetSkillsMissingToUseAllSkillsFromListRecursive(self, skills):
    579         ret = OrderedDict()
    580         self._GetSkillsMissingToUseAllSkillsFromListRecursive(skills, ret)
    581         return ret
    582 
    583     def _GetSkillsMissingToUseAllSkillsFromListRecursive(self, skills, ret):
    584         for skillTypeID, lvl in skills:
    585             if self.GetMyLevel(skillTypeID) < lvl:
    586                 if skillTypeID not in skills:
    587                     mySkill = self.GetSkillIncludingLapsed(skillTypeID)
    588                     if not mySkill or mySkill.trainedSkillPoints <= 0:
    589                         self._GetMissingSkillsRequiredToUseTypeRecursive(skillTypeID, ret)
    590                 ret[skillTypeID] = max(ret.get(skillTypeID, 0), lvl)
    591 
    592     def GetSkillTrainingTimeLeftToUseType(self, skillTypeID, includeBoosters = True):
    593         if self.IsSkillRequirementMet(skillTypeID):
    594             return 0
    595         required = self.GetRequiredSkillsRecursive(skillTypeID)
    596         totalTime = self.GetSkillTrainingTimeLeftForTypesAndLevels(required, includeBoosters)
    597         return long(totalTime)
    598 
    599     def GetSkillTrainingTimeLeftForTypesAndLevels(self, required, includeBoosters):
    600         totalTime = 0
    601         skillsIncludingLapsed = self.GetSkillsIncludingLapsed()
    602         requiredMax = {}
    603         for typeID, lvl in required.iteritems():
    604             haveSkill = skillsIncludingLapsed.get(typeID, None)
    605             if haveSkill and haveSkill.trainedSkillLevel >= lvl:
    606                 continue
    607             elif typeID not in requiredMax:
    608                 requiredMax[typeID] = int(lvl)
    609 
    610         for typeID, trainToLevel in requiredMax.iteritems():
    611             trainedSkillLevel = 0
    612             skillEntry = skillsIncludingLapsed.get(typeID, None)
    613             if skillEntry and skillEntry.trainedSkillLevel is not None:
    614                 trainedSkillLevel = skillEntry.trainedSkillLevel
    615             for trainingLevel in xrange(trainedSkillLevel + 1, trainToLevel + 1):
    616                 totalTime += self.GetRawTrainingTimeForSkillLevel(typeID, trainingLevel, includeBoosters=includeBoosters)
    617 
    618         return totalTime
    619 
    620     def GetSkillPointsRequiredForSkills(self, skillTypeIDsAndLevels):
    621 
    622         def _get_skillpoint_cost_for_level(skill_rank, skill_level):
    623             if skill_level <= 0:
    624                 return 0
    625             return GetSPForLevelRaw(skill_rank, skill_level) - GetSPForLevelRaw(skill_rank, skill_level - 1)
    626 
    627         pointsBySkillTypeID = OrderedDict()
    628         for skillTypeID, level in skillTypeIDsAndLevels:
    629             if self.GetMyLevel(skillTypeID) >= level:
    630                 continue
    631             rank = self.GetSkillRank(skillTypeID)
    632             pointsPresent = 0
    633             existingSkill = self.GetSkill(skillTypeID)
    634             if existingSkill is not None and existingSkill.trainedSkillLevel == level - 1:
    635                 pointsPresent = existingSkill.trainedSkillPoints - GetSPForLevelRaw(rank, existingSkill.trainedSkillLevel)
    636             remainingPointsToLevel = _get_skillpoint_cost_for_level(rank, level) - pointsPresent
    637             pointsBySkillTypeID[skillTypeID] = remainingPointsToLevel + pointsBySkillTypeID.get(skillTypeID, 0)
    638 
    639         return pointsBySkillTypeID
    640 
    641     def GetTrainingTimeLeftForSkillLevels(self, skillsLevels, includeBoosters = True):
    642         requiredSkills = {}
    643         for skillTypeID, level in skillsLevels.iteritems():
    644             if requiredSkills.get(skillTypeID, 0) < level:
    645                 requiredSkills[skillTypeID] = level
    646             newRequirements = self.GetRequiredSkillsRecursive(skillTypeID)
    647             for newSkillID, newLevel in newRequirements.iteritems():
    648                 if requiredSkills.get(newSkillID, 0) < newLevel:
    649                     requiredSkills[newSkillID] = newLevel
    650 
    651         totalTime = self.GetSkillTrainingTimeLeftForTypesAndLevels(requiredSkills, includeBoosters)
    652         return totalTime
    653 
    654     def GetSkillToolTip(self, skillTypeID, level):
    655         if session.charid is None:
    656             return
    657         mySkill = self.GetSkill(skillTypeID)
    658         mySkillLevel = 0
    659         if mySkill is not None:
    660             mySkillLevel = mySkill.trainedSkillLevel or 0
    661         tooltipText = evetypes.GetDescription(skillTypeID)
    662         tooltipTextList = []
    663         for i in xrange(int(mySkillLevel) + 1, int(level) + 1):
    664             timeLeft = self.GetRawTrainingTimeForSkillLevel(skillTypeID, i)
    665             tooltipTextList.append(localization.GetByLabel('UI/SkillQueue/Skills/SkillLevelAndTrainingTime', skillLevel=i, timeLeft=long(timeLeft)))
    666 
    667         levelsText = '<br>'.join(tooltipTextList)
    668         if levelsText:
    669             tooltipText += '<br><br>' + levelsText
    670         return tooltipText
    671 
    672     def GetSkillpointsPerMinute(self, skillTypeID, includeBoosters = True):
    673         primaryAttributeID = self.GetPrimarySkillAttribute(skillTypeID)
    674         secondaryAttributeID = self.GetSecondarySkillAttribute(skillTypeID)
    675         playerPrimaryAttribute = self.GetCharacterAttribute(primaryAttributeID, includeBoosters=includeBoosters)
    676         playerSecondaryAttribute = self.GetCharacterAttribute(secondaryAttributeID, includeBoosters=includeBoosters)
    677         return GetSkillPointsPerMinute(playerPrimaryAttribute, playerSecondaryAttribute)
    678 
    679     def GetRawTrainingTimeForSkillLevel(self, skillTypeID, skillLevel, includeBoosters = True):
    680         skillTimeConstant = self.GetSkillRank(skillTypeID)
    681         rawSkillPointsToTrain = GetSPForLevelRaw(skillTimeConstant, skillLevel)
    682         trainingRate = self.GetSkillpointsPerMinute(skillTypeID, includeBoosters=includeBoosters)
    683         existingSP = 0
    684         priorLevel = skillLevel - 1
    685         skillInfo = self.GetSkills().get(skillTypeID, None)
    686         if skillInfo:
    687             existingSP = GetSPForLevelRaw(skillTimeConstant, priorLevel)
    688             if priorLevel >= 0 and priorLevel == skillInfo.trainedSkillLevel:
    689                 existingSP = sm.GetService('skillqueue').GetSkillPointsFromSkillObject(skillTypeID, skillInfo)
    690         if existingSP > rawSkillPointsToTrain:
    691             return 0
    692         skillPointsToTrain = rawSkillPointsToTrain - existingSP
    693         trainingTimeInMinutes = float(skillPointsToTrain) / float(trainingRate)
    694         return trainingTimeInMinutes * const.MIN
    695 
    696     @telemetry.ZONE_METHOD
    697     def GetSkillCount(self):
    698         return len(self.GetSkills())
    699 
    700     @telemetry.ZONE_METHOD
    701     def GetSkillPoints(self, groupID = None):
    702         return sum([ skillInfo.trainedSkillPoints or 0 for skillTypeID, skillInfo in self.GetSkills().iteritems() if groupID is None or evetypes.GetGroupID(skillTypeID) == groupID ])
    703 
    704     def GetTotalSkillPointsForCharacter(self):
    705         trainedSkillPoints = sum([ skillInfo.trainedSkillPoints or 0 for skillTypeID, skillInfo in self.GetSkillsIncludingLapsed().iteritems() ])
    706         unallocatedSkillPoints = self.GetFreeSkillPoints()
    707         return trainedSkillPoints + unallocatedSkillPoints
    708 
    709     def GetTotalSkillPointsForCharacterCached(self):
    710         if not self._charTotalSkillPoints:
    711             self._charTotalSkillPoints = self.GetTotalSkillPointsForCharacter()
    712         return self._charTotalSkillPoints
    713 
    714     def GetSkillRank(self, skillTypeID):
    715         return self.godma.GetTypeAttribute(skillTypeID, attributeSkillTimeConstant)
    716 
    717     def GetPrimarySkillAttribute(self, skillTypeID):
    718         return self.godma.GetTypeAttribute(skillTypeID, attributePrimaryAttribute)
    719 
    720     def GetSecondarySkillAttribute(self, skillTypeID):
    721         return self.godma.GetTypeAttribute(skillTypeID, attributeSecondaryAttribute)
    722 
    723     def GetSkillAcceleratorBoosters(self):
    724         if self._skillAcceleratorBoosters is None:
    725             self._skillAcceleratorBoosters = self._GetSkillAcceleratorBoosters()
    726         return self._skillAcceleratorBoosters
    727 
    728     def _GetSkillAcceleratorBoosters(self):
    729         myGodmaItem = self.godma.GetItem(session.charid)
    730         skillBoosters = SkillAcceleratorBoosters(self.godma.GetTypeAttribute2)
    731         dogmaStaticMgr = sm.GetService('clientDogmaStaticSvc')
    732         for booster in myGodmaItem.boosters:
    733             if IsBoosterSkillAccelerator(dogmaStaticMgr, booster):
    734                 skillBoosters.add_booster(booster.expiryTime, booster.boosterTypeID)
    735 
    736         return skillBoosters
    737 
    738     def Train(self, skillX):
    739         skill = sm.GetService('skillqueue').SkillInTraining()
    740         if skill and prompt_player_fsd_dialog('ConfirmResetSkillTraining', {'name': evetypes.GetName(skill.typeID),
    741          'lvl': skill.skillLevel + 1}, uiconst.OKCANCEL) != uiconst.ID_OK:
    742             return
    743         self.GetSkillHandler().CharStartTrainingSkill(skillX.itemID, skillX.locationID)
    744 
    745     def InjectSkillIntoBrain(self, skillX):
    746         skillIDList = [ skill.itemID for skill in skillX ]
    747         if not skillIDList:
    748             return
    749         for skill in skillX:
    750             if not evetypes.IsPublished(skill.typeID):
    751                 raise UserError('ItemNotASkill', {'skillName': skill.typeID})
    752 
    753         try:
    754             self.godma.GetDogmaLM().InjectSkillIntoBrain(skillIDList)
    755         except UserError as e:
    756             raise
    757         else:
    758             for skill in skillX:
    759                 sm.GetService('notificationSvc').MakeAndScatterNotification(notificationConst.notificationTypeSkillInjected, data={'typeID': skill.typeID})
    760 
    761             PlaySound(uiconst.SOUND_ADD_OR_USE)
    762 
    763     def AbortTrain(self):
    764         self.GetSkillHandler().AbortTraining()
    765 
    766     @telemetry.ZONE_METHOD
    767     def GetRespecInfo(self):
    768         if self.respecInfo is None:
    769             self.respecInfo = self.GetSkillHandler().GetRespecInfo()
    770         return self.respecInfo
    771 
    772     def OnRespecInfoChanged(self, *args):
    773         self.respecInfo = None
    774         self.GetCharacterAttributes(True)
    775         sm.ScatterEvent('OnRespecInfoUpdated')
    776 
    777     def OnOpenCharacterSheet(self, skillIDs, *args):
    778         sm.GetService('charactersheet').ForceShowSkillHistoryHighlighting(skillIDs)
    779 
    780     def MakeSkillQueueEmptyNotification(self, skillQueueNotification):
    781         queueText = localization.GetByLabel('UI/SkillQueue/NoSkillsInQueue')
    782         skillQueueNotification = Notification.MakeSkillNotification(header=queueText, text='', created=blue.os.GetWallclockTime(), callBack=sm.StartService('skills').OnOpenCharacterSheet, callbackargs=None, notificationType=Notification.SKILL_NOTIFICATION_EMPTYQUEUE)
    783         return skillQueueNotification
    784 
    785     def TriggerSkillsTrainedNotification(self, levelsTrained):
    786         if len(levelsTrained) > 1:
    787             text = localization.GetByLabel('UI/CharacterSheet/CharacterSheetWindow/SkillTabs/SkillTrainingComplete')
    788             subtext = localization.GetByLabel('UI/SkillQueue/Skills/NumberOfSkills', amount=len(levelsTrained))
    789             self._TriggerSkillNotification(levelsTrained, text, subtext)
    790         else:
    791             skillTypeID, level = levelsTrained[0]
    792             text = localization.GetByLabel('UI/CharacterSheet/CharacterSheetWindow/SkillTabs/SkillTrainingComplete')
    793             subtext = localization.GetByLabel('UI/SkillQueue/Skills/SkillNameAndLevel', skill=skillTypeID, amount=level)
    794             self._TriggerSkillNotification(levelsTrained, text, subtext)
    795 
    796     def _CheckTriggerNoSkillsInQueueNotification(self):
    797         queue = sm.GetService('skillqueue').GetServerQueue()
    798         if not len(queue):
    799             emptyQueueNotification = self.MakeSkillQueueEmptyNotification(None)
    800             sm.ScatterEvent('OnNewNotificationReceived', emptyQueueNotification)
    801 
    802     def _TriggerSkillNotification(self, skillChanges, text, subtext):
    803         header = '%s - %s' % (text, subtext)
    804         skillNotification = Notification.MakeSkillNotification(header=header, text='', created=blue.os.GetWallclockTime(), callBack=sm.StartService('skills').OnOpenCharacterSheet, callbackargs=skillChanges)
    805         sm.ScatterEvent('OnNewNotificationReceived', skillNotification)
    806 
    807     def OnFreeSkillPointsChanged(self, newFreeSkillPoints):
    808         self.SetFreeSkillPoints(newFreeSkillPoints)
    809 
    810     @telemetry.ZONE_METHOD
    811     def GetFreeSkillPoints(self):
    812         if self.freeSkillPoints is None:
    813             self.freeSkillPoints = self.GetSkillHandler().GetFreeSkillPoints()
    814         return self.freeSkillPoints
    815 
    816     def ApplyFreeSkillPointsToSkills(self, skillTypeIDsAndLevels):
    817         if self.freeSkillPoints is None:
    818             self.GetFreeSkillPoints()
    819         for skillTypeID, level in skillTypeIDsAndLevels:
    820             if self.IsSkillInjected(skillTypeID) == False:
    821                 raise UserError('CannotApplyFreePointsDoNotHaveSkill', {'skillName': evetypes.GetName(skillTypeID)})
    822             skill = self.GetSkill(skillTypeID)
    823             if skill is None:
    824                 raise UserError('CannotApplyFreePointsDoNotHaveSkill', {'skillName': evetypes.GetName(skillTypeID)})
    825 
    826     def ApplyFreeSkillPoints(self, skillTypeID, pointsToApply):
    827         if self.freeSkillPoints is None:
    828             self.GetFreeSkillPoints()
    829         inTraining = sm.GetService('skillqueue').SkillInTraining()
    830         if inTraining is not None and inTraining.typeID == skillTypeID:
    831             raise UserError('CannotApplyFreePointsWhileTrainingSkill')
    832         if self.IsSkillInjected(skillTypeID) == False:
    833             raise UserError('CannotApplyFreePointsDoNotHaveSkill', {'skillName': evetypes.GetName(skillTypeID)})
    834         skill = self.GetSkill(skillTypeID)
    835         if skill is None:
    836             raise UserError('CannotApplyFreePointsDoNotHaveSkill', {'skillName': evetypes.GetName(skillTypeID)})
    837         spAtMaxLevel = GetSPForLevelRaw(skill.skillRank, 5)
    838         if skill.trainedSkillPoints + pointsToApply > spAtMaxLevel:
    839             pointsToApply = spAtMaxLevel - skill.trainedSkillPoints
    840         if pointsToApply > self.freeSkillPoints:
    841             raise UserError('CannotApplyFreePointsNotEnoughRemaining', {'pointsRequested': pointsToApply,
    842              'pointsRemaining': self.freeSkillPoints})
    843         if pointsToApply <= 0:
    844             return
    845         newFreePoints = self.GetSkillHandler().ApplyFreeSkillPoints(skill.typeID, pointsToApply)
    846         self.SetFreeSkillPoints(newFreePoints)
    847 
    848     def SetFreeSkillPoints(self, newFreePoints, showPointer = True):
    849         isFreeSkillPointsChange = self.freeSkillPoints is None or newFreePoints != self.freeSkillPoints
    850         if not isFreeSkillPointsChange:
    851             return
    852         isFreeSkillPointsIncrease = self.freeSkillPoints is None or newFreePoints > self.freeSkillPoints
    853         if isFreeSkillPointsIncrease:
    854             uthread.new(self.ShowSkillPointsNotification_thread)
    855         self.freeSkillPoints = newFreePoints
    856         sm.ScatterEvent('OnFreeSkillPointsChanged_Local')
    857         if isFreeSkillPointsIncrease and showPointer:
    858             sm.GetService('uiHighlightingService').highlight_ui_element(HIGHLIGHT_SP_ADDED)
    859 
    860     def MakeAndScatterSkillPointNotification(self):
    861         notificationData = UnusedSkillPointsFormatter.MakeData()
    862         sm.GetService('notificationSvc').MakeAndScatterNotification(type=notificationConst.notificationTypeUnusedSkillPoints, data=notificationData)
    863 
    864     def ShowSkillPointsNotification(self, number = (0, 0), time = 5000, *args):
    865         skillPointsNow = self.GetFreeSkillPoints()
    866         skillPointsLast = settings.user.ui.Get('freeSkillPoints', -1)
    867         if skillPointsLast == skillPointsNow:
    868             return
    869         if skillPointsNow <= 0:
    870             return
    871         self.MakeAndScatterSkillPointNotification()
    872         settings.user.ui.Set('freeSkillPoints', skillPointsNow)
    873 
    874     def ShowSkillPointsNotification_thread(self):
    875         blue.pyos.synchro.SleepWallclock(5000)
    876         if session.charid:
    877             self.ShowSkillPointsNotification()
    878 
    879     def OnSkillForcedRefresh(self):
    880         uthread.Pool('skillSvc::OnSkillForcedRefresh', self.ForceRefresh)
    881 
    882     def ForceRefresh(self):
    883         self.Reset()
    884         sm.ScatterEvent('OnSkillQueueRefreshed')
    885 
    886     def OnServerBoostersChanged(self, *args):
    887         self.GetCharacterAttributes(True)
    888         self._skillAcceleratorBoosters = None
    889         sm.ScatterEvent('OnBoosterUpdated')
    890         sm.ScatterEvent('OnSkillQueueRefreshed')
    891 
    892     def OnServerImplantsChanged(self, *args):
    893         skillQueueSvc = sm.GetService('skillqueue')
    894         self.GetCharacterAttributes(True)
    895         if skillQueueSvc.GetQueue():
    896             sm.ScatterEvent('OnSkillQueueRefreshed')
    897         sm.ScatterEvent('OnImplantsChanged')
    898 
    899     def OnCloneDestruction(self, *args):
    900         self.GetCharacterAttributes(True)
    901         sm.ScatterEvent('OnBoosterUpdated')
    902 
    903     def OnJumpCloneTransitionCompleted(self):
    904         self.GetCharacterAttributes(True)
    905         sm.ScatterEvent('OnBoosterUpdated')
    906 
    907     def GetBoosters(self, forced = 0):
    908         if self.boosters is None or forced:
    909             self.boosters = self.GetSkillHandler().GetBoosters()
    910         return self.boosters
    911 
    912     def GetImplants(self, forced = 0):
    913         if self.implants is None or forced:
    914             self.implants = self.GetSkillHandler().GetImplants()
    915         return self.implants
    916 
    917     def ExtractSkills(self, skills, itemID):
    918         skills = {s:p for s, p in skills.iteritems()}
    919         token = sm.GetService('connection').GetAccessToken()
    920         self.GetSkillHandler().ExtractSkills(skills, itemID, token)
    921 
    922     def ActivateSkillInjector(self, itemID, quantity):
    923         self.GetSkillHandler().InjectSkillpoints(itemID, quantity)
    924         sm.GetService('audio').SendUIEvent('st_activate_skill_injector_play')
    925         sm.ScatterEvent('OnSkillPointsInjected_Local', quantity)
    926 
    927     def CheckInjectionConstraints(self, itemID, quantity):
    928         self.GetSkillHandler().CheckInjectionConstraints(itemID, quantity)
    929 
    930     def ActivateSkillExtractor(self, item):
    931         if eveCfg.InSpace():
    932             raise UserError('SkillExtractorNotDockedInStation', {'extractor': const.typeSkillExtractor})
    933         skillPoints = self.GetSkillHandler().GetSkillPoints()
    934         freeSkillPoints = self.GetSkillHandler().GetFreeSkillPoints()
    935         if skillPoints + freeSkillPoints < const.SKILL_TRADING_MINIMUM_SP_TO_EXTRACT:
    936             raise UserError('SkillExtractionNotEnoughSP', {'limit': const.SKILL_TRADING_MINIMUM_SP_TO_EXTRACT,
    937              'extractor': const.typeSkillExtractor})
    938         if blue.pyos.packaged:
    939             token = sm.GetService('connection').GetAccessToken()
    940             if token is None:
    941                 raise UserError('TokenRequiredForSkillExtraction')
    942         SkillExtractorWindow.OpenOrReload(itemID=item.itemID)
    943 
    944     def GetSkillPointAmountFromInjectors(self, typeID, quantity):
    945         nonDiminishingInjectionsRemaining = self.nonDiminishingInjection.GetRemaining()
    946         return self.GetSkillHandler().GetDiminishedSpFromInjectors(typeID, quantity, nonDiminishingInjectionsRemaining)
    947 
    948     def GetNextAlphaInjectionDateTime(self):
    949         return sm.RemoteSvc('alphaInjectorMgr').GetNextAvailableInjection()
    950 
    951     def GetDurationUntilNextAlphaInjection(self):
    952         return max(0, self.GetNextAlphaInjectionDateTime() - blue.os.GetWallclockTime())
    953 
    954     def SplitSkillInjector(self, itemID, quantity):
    955         self.GetSkillHandler().SplitSkillInjector(itemID, quantity)
    956 
    957     def CombineSkillInjector(self, itemID, quantity):
    958         if quantity < const.SKILL_TRADING_SMALL_INJECTOR_DIVISOR:
    959             raise UserError('CombineSkillInjectorTooFewInjectors', {'minQuantity': const.SKILL_TRADING_SMALL_INJECTOR_DIVISOR,
    960              'injectorType': const.typeSmallSkillInjector})
    961         self.GetSkillHandler().CombineSkillInjector(itemID, quantity)
    962 
    963     def GetSkillTrainingTimeCalculator(self):
    964         return SkillTrainingTimeCalculator(ClientCharacterSkillInterface(self))
    965 
    966     def GetTypesUnlockedByTrainingToLevel(self, skillTypeID, level, includeQueue = True):
    967         typeIndex = get_required_skills_index().get(skillTypeID, {})
    968         typeIndex = typeIndex.get(level, {})
    969         if includeQueue:
    970             maxQueueIdx = self.GetSkillInQueueIndex(skillTypeID, level)
    971             skillsInQueue = self.GetMaxSkillLevelsInQueue(maxQueueIdx)
    972         else:
    973             skillsInQueue = {}
    974         ret = defaultdict(dict)
    975         for marketGroupID, typesByMetaGroupID in typeIndex.iteritems():
    976             for metaGroupID, typeIDs in typesByMetaGroupID.iteritems():
    977                 publishedTypeIDs = [ typeID for typeID in typeIDs if self._IsUnlockedType(skillTypeID, typeID, skillsInQueue) ]
    978                 if publishedTypeIDs:
    979                     ret[marketGroupID][metaGroupID] = publishedTypeIDs
    980 
    981         return dict(ret)
    982 
    983     def _IsUnlockedType(self, skillID, typeID, skillsInQueue):
    984         required = self.GetRequiredSkills(typeID)
    985         for requiredSkillID, requiredLevel in required.iteritems():
    986             if requiredSkillID == skillID:
    987                 continue
    988             elif self.MySkillLevel(requiredSkillID) < requiredLevel:
    989                 if skillsInQueue.get(requiredSkillID, 0) < requiredLevel:
    990                     return False
    991 
    992         return True
    993 
    994     def _ConstructSkillControllers(self):
    995         self.skillsByGroupIDTypeID = defaultdict(dict)
    996         typeIDs = evetypes.GetTypeIDsByCategory(invconst.categorySkill)
    997         for typeID in typeIDs:
    998             if not evetypes.IsPublished(typeID) and not self.GetSkill(typeID):
    999                 continue
   1000             groupID = evetypes.GetGroupID(typeID)
   1001             self.skillsByGroupIDTypeID[groupID][typeID] = SkillController(typeID)
   1002 
   1003     def GetSkillControllers(self):
   1004         if self.skillsByGroupIDTypeID is None:
   1005             self._ConstructSkillControllers()
   1006         return self.skillsByGroupIDTypeID
   1007 
   1008     def OnAlphaInjectionAvailable(self):
   1009         uthread.new(self._NotifyAlphaInjectionAvailable)
   1010 
   1011     def _NotifyAlphaInjectionAvailable(self):
   1012         notification = SimpleNotification(subject=localization.GetByLabel('UI/SkillTrading/AlphaInjectionAvailable'), created=blue.os.GetWallclockTime(), notificationID=None, notificationTypeID=notificationconst.notificationTypeAlphaInjectorAvailable)
   1013         sm.ScatterEvent('OnNewNotificationReceived', notification)
   1014 
   1015     def PurchaseSkills(self, skillTypeIDs, confirm = True, skillPlanTemplate = None):
   1016         context = characterskills.client.SkillPurchaseContext(skill_service=self, skill_plan_template=skillPlanTemplate)
   1017         return characterskills.client.purchase_skills(context, skillTypeIDs, confirm=confirm)
   1018 
   1019     def WaitUntilSkillsAreAvailable(self, skillTypeIDs):
   1020         skill_type_ids = set(skillTypeIDs)
   1021         seen = set()
   1022         while skill_type_ids != seen:
   1023             blue.pyos.BeNice()
   1024             for skill_type_id in skill_type_ids:
   1025                 if self.GetSkillIncludingLapsed(skill_type_id) is None:
   1026                     break
   1027                 seen.add(skill_type_id)
   1028 
   1029         sm.ScatterEvent('OnSkillsAvailable', skillTypeIDs)
   1030 
   1031     def GetSkillBundleInfo(self, skillBundleID):
   1032         return self.GetSkillHandler().GetSkillBundleInfo(skillBundleID)
   1033 
   1034     def GetSkillBundleHighlight(self, numberOfSkills, sp):
   1035         if numberOfSkills > 0 and sp > 0:
   1036             return HIGHLIGHT_SKILLS_TRAINED_AND_SP_ADDED
   1037         if numberOfSkills > 0:
   1038             return HIGHLIGHT_SKILLS_TRAINED
   1039         if sp > 0:
   1040             return HIGHLIGHT_SP_ADDED
   1041 
   1042     def OnSkillBundleInjected(self, numberOfSkills, sp):
   1043         highlight_id = self.GetSkillBundleHighlight(numberOfSkills, sp)
   1044         if highlight_id:
   1045             sm.GetService('uiHighlightingService').highlight_ui_element(highlight_id)
   1046 
   1047     def GetTrainingTimeForSkills(self, skills):
   1048         trainingTime = 0
   1049         for skillID, level in skills.iteritems():
   1050             try:
   1051                 myLevel = self.GetSkillIncludingLapsed(skillID).trainedSkillLevel or 0
   1052             except AttributeError:
   1053                 myLevel = 0
   1054 
   1055             for i in xrange(int(myLevel) + 1, int(level) + 1):
   1056                 trainingTime += self.GetRawTrainingTimeForSkillLevel(skillID, i)
   1057 
   1058         return trainingTime
   1059 
   1060     def GetTrainingTimeForSkillsExcludeTrained(self, skills):
   1061         trainingTime = 0
   1062         for skillID, level in skills.iteritems():
   1063             for i in xrange(1, int(level) + 1):
   1064                 trainingTime += self.GetRawTrainingTimeForSkillLevel(skillID, i)
   1065 
   1066         return trainingTime