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

skillQueueSvc.py (49576B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\ui\services\skillQueueSvc.py
      2 import sys
      3 from contextlib import contextmanager
      4 import blue
      5 import carbonui.const as uiconst
      6 import evetypes
      7 import gametime
      8 import localization
      9 from carbon.client.script.environment.AudioUtil import PlaySound
     10 from carbon.common.script.sys.service import Service
     11 from carbonui.util.various_unsorted import GetAttrs
     12 from characterskills.queue import SKILLQUEUE_MAX_NUM_SKILLS, GetQueueEntry, GetSkillQueueTimeLength
     13 from characterskills.skill_training import SkillLevelAlreadyTrainedError
     14 from characterskills.util import GetSkillLevelRaw, GetLevelProgress
     15 from eve.client.script.ui.util import uix
     16 from eveexceptions import UserError
     17 from menu import MenuLabel
     18 from omega.client.clone_grade_svc import get_clone_grade_svc
     19 from player_messaging.client.ui_message import prompt_player_fsd_dialog
     20 from skills import skillConst
     21 from skills.client.errors import SkillQueueOverwritten
     22 from textImporting import IsUsingDefaultLanguage
     23 from textImporting.importSkillplan import ImportSkillPlan
     24 from uthread import UnLock, ReentrantLock
     25 from utillib import KeyVal
     26 
     27 class SkillQueueService(Service):
     28     __exportedcalls__ = {'SkillInTraining': []}
     29     __guid__ = 'svc.skillqueue'
     30     __servicename__ = 'skillqueue'
     31     __displayname__ = 'Skill Queue Client Service'
     32     __dependencies__ = ['godma', 'skills', 'machoNet']
     33     __notifyevents__ = ['OnSkillQueuePausedServer',
     34      'OnMultipleCharactersTrainingUpdated',
     35      'OnNewSkillQueueSaved',
     36      'OnSubscriptionChanged',
     37      'OnCharacterSessionChanged',
     38      'OnSessionReset']
     39 
     40     def __init__(self):
     41         super(SkillQueueService, self).__init__()
     42         self.skillQueue = []
     43         self.cachedSkillQueue = None
     44         self.skillQueueCache = None
     45         self.skillplanImporter = None
     46         self.maxSkillqueueTimeLength = None
     47         self._isAllCharacterTrainingSlotsUsed = None
     48 
     49     def Run(self, memStream = None):
     50         self.PrimeSkillQueue()
     51 
     52     def ReInitialize(self):
     53         self.skillQueue = []
     54         self.cachedSkillQueue = None
     55         self.skillQueueCache = None
     56         self.skillplanImporter = None
     57         self.maxSkillqueueTimeLength = None
     58         self._isAllCharacterTrainingSlotsUsed = None
     59 
     60     def OnCharacterSessionChanged(self, _oldCharacterID, newCharacterID):
     61         self.ReInitialize()
     62         self.PrimeCache()
     63         if newCharacterID is not None:
     64             self.PrimeSkillQueue()
     65 
     66     def OnSessionReset(self):
     67         self.ReInitialize()
     68 
     69     def PrimeSkillQueue(self):
     70         self.skillQueue, freeSkillPoints = self.GetSkillQueueAndFreeSPsFromServer()
     71         if freeSkillPoints is not None and freeSkillPoints > 0:
     72             self.skills.SetFreeSkillPoints(freeSkillPoints, showPointer=False)
     73 
     74     def GetMaxSkillQueueLimitLength(self):
     75         if not self.maxSkillqueueTimeLength:
     76             self.maxSkillqueueTimeLength = GetSkillQueueTimeLength(get_clone_grade_svc().get_clone_grade_with_grace_period())
     77         return self.maxSkillqueueTimeLength
     78 
     79     def OnSubscriptionChanged(self):
     80         self.maxSkillqueueTimeLength = None
     81 
     82     def _ValidateCache(self):
     83         if self.IsTransactionOpen():
     84             raise SkillQueueOverwritten(session.charid, self.cachedSkillQueue)
     85 
     86     def BeginTransaction(self):
     87         with self.ChangeQueueLock():
     88             sendEvent = False
     89             skillInTraining = self.SkillInTraining()
     90             try:
     91                 self._ValidateCache()
     92             except SkillQueueOverwritten as exc:
     93                 sendEvent = True
     94                 self.LogException(exc)
     95 
     96             self.skillQueueCache = None
     97             self.skillQueue, freeSkillPoints = self.GetSkillQueueAndFreeSPsFromServer()
     98             if freeSkillPoints > 0:
     99                 self.skills.SetFreeSkillPoints(freeSkillPoints)
    100             self.cachedSkillQueue = self.GetQueue()
    101             if not skillInTraining:
    102                 self.UpdateQueueTimestamps()
    103             if sendEvent:
    104                 sm.ScatterEvent('OnSkillQueueRefreshed')
    105 
    106     def GetSkillQueueAndFreeSPsFromServer(self):
    107         return self.skills.GetSkillHandler().GetSkillQueueAndFreePoints()
    108 
    109     def UpdateQueueTimestamps(self):
    110         queueWithTimestamps = []
    111         for idx, trainingSkill in enumerate(self.skillQueue):
    112             startTime = None
    113             if idx > 0:
    114                 startTime = self.skillQueue[idx - 1].trainingEndTime
    115             currentSkill = self.skills.GetSkillIncludingLapsed(trainingSkill.trainingTypeID)
    116             queueEntry = GetQueueEntry(trainingSkill.trainingTypeID, trainingSkill.trainingToLevel, idx, currentSkill, queueWithTimestamps, lambda x, y: self.GetTimeForTraining(x, y, startTime), KeyVal, True)
    117             queueWithTimestamps.append(queueEntry)
    118 
    119         self.skillQueue = queueWithTimestamps
    120 
    121     def RollbackTransaction(self):
    122         with self.ChangeQueueLock():
    123             if self.cachedSkillQueue is None:
    124                 self.LogError('%s: Cannot rollback a skill queue transaction - no transaction was opened!' % session.charid)
    125                 return
    126             self.skillQueue = self.cachedSkillQueue
    127             self.skillQueueCache = None
    128             self.cachedSkillQueue = None
    129 
    130     def CommitTransaction(self, activate = True):
    131         with self.ChangeQueueLock():
    132             if activate:
    133                 self._CheckCloneGradeRestrictions()
    134             self.PrimeCache(force=True)
    135             if self.IsQueueChanged() or activate:
    136                 try:
    137                     if activate:
    138                         self.TrimQueue()
    139                     queueInfo = {idx:(x.trainingTypeID, x.trainingToLevel) for idx, x in enumerate(self.skillQueue)}
    140                     skillHandler = self.skills.GetSkillHandler()
    141                     skillHandler.SaveNewQueue(queueInfo, activate=activate)
    142                     self.cachedSkillQueue = None
    143                     sm.ScatterEvent('OnSkillQueueRefreshed')
    144                 except Exception as e:
    145                     msg = getattr(e, 'msg', None)
    146                     if msg not in ('UserAlreadyHasSkillInTraining', 'SkillInQueueRequiresOmegaCloneState', 'SkillInQueueOverAlphaSpTrainingSize'):
    147                         self.RollbackTransaction()
    148                         sm.ScatterEvent('OnSkillQueueRefreshed')
    149                     raise
    150 
    151     def _CheckCloneGradeRestrictions(self):
    152         if get_clone_grade_svc().is_omega_with_grace_period():
    153             return
    154         for skill in self.skillQueue:
    155             levelLimit = get_clone_grade_svc().get_max_skill_level(skill.trainingTypeID)
    156             if levelLimit < skill.trainingToLevel:
    157                 raise UserError('SkillInQueueRequiresOmegaCloneState')
    158 
    159     def CheckCanAppendSkill(self, skillTypeID, skillLevel = None, check = False, performLengthTest = True):
    160         if skillLevel is None:
    161             skillLevel = self.FindNextLevel(skillTypeID, skillQueue=self.GetQueue())
    162         position = len(self.skillQueue)
    163         return self.CheckCanInsertSkillAtPosition(skillTypeID, skillLevel, position, check, performLengthTest)
    164 
    165     def CheckCanInsertSkillAtPosition(self, skillTypeID, skillLevel, position = None, check = 0, performLengthTest = True):
    166         if position is None or position < 0 or position > len(self.skillQueue):
    167             raise UserError('QueueInvalidPosition')
    168         self.PrimeCache()
    169         mySkill = self.skills.GetSkillIncludingLapsed(skillTypeID)
    170         ret = True
    171         try:
    172             if mySkill is None:
    173                 raise UserError('QueueSkillNotUploaded')
    174             if mySkill.trainedSkillLevel >= skillLevel:
    175                 raise UserError('QueueCannotTrainPreviouslyTrainedSkills')
    176             if mySkill.trainedSkillLevel >= skillConst.skill_max_level:
    177                 raise UserError('QueueCannotTrainPastMaximumLevel', {'typeName': (const.UE_TYPEID, skillTypeID)})
    178             if skillTypeID in self.skillQueueCache:
    179                 for lvl, lvlPosition in self.skillQueueCache[skillTypeID].iteritems():
    180                     if lvl < skillLevel and lvlPosition >= position:
    181                         raise UserError('QueueCannotPlaceSkillLevelsOutOfOrder')
    182                     elif lvl > skillLevel and lvlPosition < position:
    183                         raise UserError('QueueCannotPlaceSkillLevelsOutOfOrder')
    184 
    185             if position >= 0 and performLengthTest:
    186                 timeLeft, _ = self.GetTrainingLengthOfQueue(position)
    187                 if timeLeft > self.GetMaxSkillQueueLimitLength():
    188                     raise UserError('QueueTooLong')
    189             if mySkill.trainedSkillPoints == 0:
    190                 requirements = sm.GetService('skills').GetRequiredSkills(skillTypeID)
    191                 for requiredTypeID, requiredLevel in requirements.iteritems():
    192                     isRequirementsQueued = False
    193                     if requiredTypeID in self.skillQueueCache:
    194                         for level, queuedPosition in self.skillQueueCache[requiredTypeID].iteritems():
    195                             if level <= requiredLevel and queuedPosition > position:
    196                                 raise UserError('QueueCannotPlaceSkillBeforeRequirements')
    197                             if level >= requiredLevel and queuedPosition < position:
    198                                 isRequirementsQueued = True
    199 
    200                     skill = self.skills.GetSkill(requiredTypeID)
    201                     if skill is None:
    202                         raise UserError('QueueCannotPlaceSkillBeforeRequirements')
    203                     isRequirementsTrained = skill.trainedSkillLevel >= requiredLevel
    204                     if not isRequirementsTrained and not isRequirementsQueued:
    205                         raise UserError('QueueCannotPlaceSkillBeforeRequirements')
    206 
    207             dependencies = sm.GetService('skills').GetDependentSkills(skillTypeID)
    208             for dependentTypeID, requiredLevel in dependencies.iteritems():
    209                 if dependentTypeID not in self.skillQueueCache:
    210                     continue
    211                 for level, queuedPosition in self.skillQueueCache[dependentTypeID].iteritems():
    212                     if requiredLevel == skillLevel and queuedPosition < position:
    213                         raise UserError('QueueCannotPlaceSkillAfterDependentSkills')
    214 
    215             if get_clone_grade_svc().is_skill_level_restricted(skillTypeID, skillLevel):
    216                 raise UserError('QueueCannotTrainOmegaRestrictedSkill', {'skillID': skillTypeID})
    217         except UserError as ue:
    218             checkedErrors = ('QueueCannotPlaceSkillAfterDependentSkills', 'QueueCannotPlaceSkillBeforeRequirements', 'QueueCannotPlaceSkillLevelsOutOfOrder', 'QueueCannotTrainPreviouslyTrainedSkills', 'QueueSkillNotUploaded', 'QueueTooLong', 'QueueCannotTrainOmegaRestrictedSkill', 'QueueCannotTrainPastMaximumLevel')
    219             if check and ue.msg in checkedErrors:
    220                 sys.exc_clear()
    221                 ret = False
    222             else:
    223                 raise
    224 
    225         return ret
    226 
    227     def AddSkillToQueue(self, skillTypeID, skillLevel = None, position = None):
    228         if not evetypes.IsPublished(skillTypeID):
    229             raise UserError('ItemNotASkill', {'skillName': skillTypeID})
    230         if skillLevel is None:
    231             skillLevel = self.FindNextLevel(skillTypeID, skillQueue=self.GetQueue())
    232         if skillLevel > skillConst.skill_max_level:
    233             return
    234         if self.FindInQueue(skillTypeID, skillLevel) is not None:
    235             raise UserError('QueueSkillAlreadyPresent')
    236         skillQueueLength = len(self.skillQueue)
    237         if skillQueueLength >= SKILLQUEUE_MAX_NUM_SKILLS:
    238             raise UserError('QueueTooManySkills', {'num': SKILLQUEUE_MAX_NUM_SKILLS})
    239         newPos = position if position is not None and position >= 0 else skillQueueLength
    240         currentSkill = self.skills.GetSkill(skillTypeID)
    241         self.CheckCanInsertSkillAtPosition(skillTypeID, skillLevel, newPos)
    242         startTime = None
    243         if newPos != 0:
    244             startTime = self.skillQueue[newPos - 1].trainingEndTime
    245         queueEntry = GetQueueEntry(skillTypeID, skillLevel, newPos, currentSkill, self.skillQueue, lambda x, y: self.GetTimeForTraining(x, y, startTime), KeyVal, self.SkillInTraining() is not None)
    246         if newPos == skillQueueLength:
    247             self.skillQueue.append(queueEntry)
    248         else:
    249             if newPos > skillQueueLength:
    250                 raise UserError('QueueInvalidPosition')
    251             self.skillQueue.insert(newPos, queueEntry)
    252             for entry in self.skillQueue[newPos + 1:]:
    253                 entry.queuePosition += 1
    254 
    255             self.skillQueueCache = None
    256         self.AddToCache(skillTypeID, skillLevel, newPos)
    257         self.TrimQueue()
    258         self.OnClientQueueModified()
    259         PlaySound(uiconst.SOUND_ADD_OR_USE)
    260         if newPos == 0:
    261             sm.ScatterEvent('OnClientEvent_SkillPrioritized', skillTypeID)
    262         sm.ScatterEvent('OnClientEvent_SkillAddedToQueue', skillTypeID, skillLevel)
    263         return newPos
    264 
    265     def AddSkillsToQueue(self, skills, ignoreAlreadyPresent = False):
    266         numSkillsAdded = 0
    267         try:
    268             while skills:
    269                 skillID, level = skills.pop(0)
    270                 if level is None:
    271                     level = self.FindNextLevel(skillID, skillQueue=self.GetQueue())
    272                 if self.FindInQueue(skillID, level) is not None:
    273                     if ignoreAlreadyPresent:
    274                         continue
    275                     raise UserError('QueueSkillAlreadyPresent')
    276                 skillQueueLength = len(self.skillQueue)
    277                 if skillQueueLength >= SKILLQUEUE_MAX_NUM_SKILLS:
    278                     if numSkillsAdded < 1:
    279                         raise UserError('QueueTooManySkills', {'num': SKILLQUEUE_MAX_NUM_SKILLS})
    280                     self.TrimQueue()
    281                     self.OnClientQueueModified()
    282                     skillList = '<br>'.join([ '- %s' % localization.GetByLabel('UI/InfoWindow/SkillAndLevel', skill=typeID, skillLevel=lvl) for typeID, lvl in [(skillID, level)] + skills ])
    283                     raise UserError('QueueTooManySkillNotAllAdded', {'maxNumSkills': SKILLQUEUE_MAX_NUM_SKILLS,
    284                      'skillList': skillList})
    285                 position = len(self.skillQueue)
    286                 self.CheckCanInsertSkillAtPosition(skillID, level, position)
    287                 startTime = None
    288                 if position > 0:
    289                     startTime = self.skillQueue[position - 1].trainingEndTime
    290                 entry = GetQueueEntry(skillID, level, position, self.skills.GetSkillIncludingLapsed(skillID), self.skillQueue, lambda x, y: self.GetTimeForTraining(x, y, startTime), KeyVal, self.SkillInTraining() is not None)
    291                 self.skillQueue.append(entry)
    292                 numSkillsAdded += 1
    293                 self.AddToCache(skillID, level, position)
    294 
    295             PlaySound(uiconst.SOUND_ADD_OR_USE)
    296         except Exception:
    297             self.LogException()
    298             return numSkillsAdded
    299 
    300         self.TrimQueue()
    301         self.OnClientQueueModified()
    302         return numSkillsAdded
    303 
    304     def AddSkillAndRequirementsToQueue(self, skillTypeID, skillLevel = None):
    305         if not self.IsTransactionOpen():
    306             self.BeginTransaction()
    307         if skillLevel is None:
    308             skillLevel = self.FindNextLevel(skillTypeID, skillQueue=self.GetQueue())
    309         if skillLevel > skillConst.skill_max_level:
    310             return
    311         required = self.skills.GetSkillsMissingToUseItemRecursive(skillTypeID)
    312         required[skillTypeID] = skillLevel
    313 
    314         def already_trained_or_queued(x):
    315             skillID, level = x
    316             skill = self.skills.GetSkillIncludingLapsed(skillID)
    317             if skill is None or skill.trainedSkillLevel is None or skill.trainedSkillLevel >= level:
    318                 return False
    319             queued = self.skills.GetMaxSkillLevelsInQueue()
    320             if queued.get(skillID, 0) >= level:
    321                 return False
    322             return True
    323 
    324         skills = filter(already_trained_or_queued, required.items())
    325         skillList = []
    326         while skills:
    327             skillID, level = skills.pop(0)
    328             required = self.skills.GetRequiredSkillsRecursive(skillID)
    329             if any((sid in required for sid, _ in skills)):
    330                 skills.append((skillID, level))
    331                 continue
    332             nextLevel = int(self.FindNextLevel(skillID, skillQueue=self.GetQueue()))
    333             for lvl in xrange(nextLevel, int(level + 1)):
    334                 skillList.append((skillID, lvl))
    335 
    336         self.AddSkillsToQueue(skillList)
    337 
    338     def IsTransactionOpen(self):
    339         return self.cachedSkillQueue is not None
    340 
    341     def RemoveSkillFromQueue(self, skillTypeID, skillLevel):
    342         self.CheckCanRemoveSkillFromQueue(skillTypeID, skillLevel)
    343         self.InternalRemoveFromQueue(skillTypeID, skillLevel)
    344         self.OnClientQueueModified()
    345         PlaySound(uiconst.SOUND_REMOVE)
    346         sm.ScatterEvent('OnClientEvent_SkillsRemovedFromQueue', ((skillTypeID, skillLevel),))
    347 
    348     def RemoveSkillsFromQueue(self, skills):
    349         removed = []
    350         try:
    351             for typeID, level in skills:
    352                 self.CheckCanRemoveSkillFromQueue(typeID, level)
    353                 self.InternalRemoveFromQueue(typeID, level)
    354                 removed.append((typeID, level))
    355 
    356         finally:
    357             if removed:
    358                 self.OnClientQueueModified()
    359                 PlaySound(uiconst.SOUND_REMOVE)
    360                 sm.ScatterEvent('OnClientEvent_SkillsRemovedFromQueue', removed)
    361 
    362     def OnClientQueueModified(self):
    363         if self.IsAllCharacterTrainingSlotsUsed():
    364             activate = False
    365         else:
    366             activate = True
    367         try:
    368             self.CommitTransaction(activate)
    369             self.BeginTransaction()
    370             sm.ScatterEvent('OnSkillQueueModified')
    371         except UserError as e:
    372             if e.msg == 'SkillInQueueRequiresOmegaCloneState':
    373                 sm.ScatterEvent('OnSkillQueueModified')
    374             elif e.msg == 'SkillInQueueOverAlphaSpTrainingSize':
    375                 self.CommitTransaction(activate=False)
    376                 self.BeginTransaction()
    377                 sm.ScatterEvent('OnSkillQueueModified')
    378             else:
    379                 raise
    380 
    381     def CheckCanRemoveSkillFromQueue(self, skillTypeID, skillLevel):
    382         self.PrimeCache()
    383         if skillTypeID not in self.skillQueueCache:
    384             return
    385         for cacheLevel in self.skillQueueCache[skillTypeID]:
    386             if cacheLevel > skillLevel:
    387                 raise UserError('QueueCannotRemoveSkillsWithHigherLevelsStillInQueue')
    388 
    389         dependencies = sm.GetService('skills').GetDependentSkills(skillTypeID)
    390         for dependentTypeID, requiredLevel in dependencies.iteritems():
    391             if skillLevel <= requiredLevel and dependentTypeID in self.skillQueueCache:
    392                 raise UserError('QueueCannotRemoveSkillsWithDependentSkillsInQueue')
    393 
    394     def FindInQueue(self, skillTypeID, skillLevel):
    395         self.PrimeCache()
    396         if skillTypeID not in self.skillQueueCache:
    397             return None
    398         if skillLevel not in self.skillQueueCache[skillTypeID]:
    399             return None
    400         return self.skillQueueCache[skillTypeID][skillLevel]
    401 
    402     def MoveSkillToPosition(self, skillTypeID, skillLevel, position):
    403         self.CheckCanInsertSkillAtPosition(skillTypeID, skillLevel, position)
    404         self.PrimeCache()
    405         currentPosition = self.skillQueueCache[skillTypeID][skillLevel]
    406         if currentPosition < position:
    407             position -= 1
    408         self.InternalRemoveFromQueue(skillTypeID, skillLevel)
    409         newPosition = self.AddSkillToQueue(skillTypeID, skillLevel, position)
    410         return newPosition
    411 
    412     def GetQueue(self):
    413         return self.skillQueue[:]
    414 
    415     def GetQueueAsRequirements(self):
    416         return [ (s.trainingTypeID, s.trainingToLevel) for s in self.GetQueue() ]
    417 
    418     def GetSkillLevelsInQueue(self):
    419         return {skill.trainingTypeID:skill.trainingToLevel for skill in self.GetQueue()}
    420 
    421     def GetServerQueue(self):
    422         if self.IsTransactionOpen():
    423             return self.cachedSkillQueue[:]
    424         else:
    425             return self.GetQueue()
    426 
    427     def GetNumberOfSkillsInQueue(self):
    428         return len(self.skillQueue)
    429 
    430     def GetTrainingLengthOfQueue(self, position = None):
    431         if position is not None and position < 0:
    432             raise RuntimeError('Invalid queue position: ', position)
    433         trainingTime = 0
    434         spInQueue = 0
    435         skillBoosters = self.GetSkillAcceleratorBoosters()
    436         playerTheoreticalSkillPoints = {}
    437         skills = self.skills.GetSkillsIncludingLapsed()
    438         currentIndex = 0
    439         finalIndex = position
    440         if finalIndex is None:
    441             finalIndex = len(self.skillQueue)
    442         for trainingSkill in self.skillQueue:
    443             queueSkillTypeID = trainingSkill.trainingTypeID
    444             queueSkillLevel = trainingSkill.trainingToLevel
    445             if currentIndex >= finalIndex:
    446                 break
    447             currentIndex += 1
    448             if queueSkillTypeID not in playerTheoreticalSkillPoints:
    449                 skill = self.skills.GetSkill(queueSkillTypeID)
    450                 playerTheoreticalSkillPoints[queueSkillTypeID] = self.GetSkillPointsFromSkillObject(queueSkillTypeID, skill)
    451             addedSP, addedTime, isAccelerated = self.GetAddedSpAndAddedTimeForSkill(queueSkillTypeID, queueSkillLevel, skills, playerTheoreticalSkillPoints, trainingTime, skillBoosters)
    452             trainingTime += addedTime
    453             spInQueue += addedSP
    454             playerTheoreticalSkillPoints[queueSkillTypeID] += addedSP
    455 
    456         return (trainingTime, spInQueue)
    457 
    458     def GetTrainingEndTimeOfQueue(self):
    459         timeLeft, _ = self.GetTrainingLengthOfQueue()
    460         timeEnd = gametime.GetWallclockTime() + timeLeft
    461         inTraining = self.SkillInTraining()
    462         if inTraining and self.FindInQueue(inTraining.typeID, inTraining.trainedSkillLevel + 1) > 0:
    463             fullTrainingTime = self.GetTrainingLengthOfSkill(inTraining.typeID, inTraining.trainedSkillLevel + 1)
    464             ETA = self.GetEndOfTraining(inTraining.typeID)
    465             if ETA is not None:
    466                 timeEnd -= fullTrainingTime[1]
    467                 leftTime = ETA - blue.os.GetWallclockTime()
    468                 timeEnd += leftTime
    469         return long(timeEnd)
    470 
    471     def GetTrainingLengthOfSkill(self, skillTypeID, skillLevel, position = None):
    472         if position is not None and (position < 0 or position > len(self.skillQueue)):
    473             raise RuntimeError('GetTrainingLengthOfSkill received an invalid position.')
    474         trainingTime = 0
    475         currentIndex = 0
    476         targetIndex = position
    477         if targetIndex is None:
    478             targetIndex = self.FindInQueue(skillTypeID, skillLevel)
    479             if targetIndex is None:
    480                 targetIndex = len(self.skillQueue)
    481         playerTheoreticalSkillPoints = {}
    482         skills = self.skills.GetSkillsIncludingLapsed()
    483         skillBoosters = self.GetSkillAcceleratorBoosters()
    484         for trainingSkill in self.skillQueue:
    485             queueSkillTypeID = trainingSkill.trainingTypeID
    486             queueSkillLevel = trainingSkill.trainingToLevel
    487             if currentIndex >= targetIndex:
    488                 break
    489             elif queueSkillTypeID == skillTypeID and queueSkillLevel == skillLevel and currentIndex < targetIndex:
    490                 currentIndex += 1
    491                 continue
    492             addedSP, addedTime, _ = self.GetAddedSpAndAddedTimeForSkill(queueSkillTypeID, queueSkillLevel, skills, playerTheoreticalSkillPoints, trainingTime, skillBoosters)
    493             currentIndex += 1
    494             trainingTime += addedTime
    495             playerTheoreticalSkillPoints[queueSkillTypeID] += addedSP
    496 
    497         addedSP, addedTime, isAccelerated = self.GetAddedSpAndAddedTimeForSkill(skillTypeID, skillLevel, skills, playerTheoreticalSkillPoints, trainingTime, skillBoosters)
    498         trainingTime += addedTime
    499         return (long(trainingTime), long(addedTime), isAccelerated)
    500 
    501     def GetSkillPointsAndTimeNeededToTrain(self, skillTypeID, skillLevel, existingSkillPoints = 0, trainingStartTime = None):
    502         calculator = self.skills.GetSkillTrainingTimeCalculator()
    503         try:
    504             if existingSkillPoints:
    505                 skillPointsToTrain, trainingTime = calculator.get_skill_points_and_time_to_train_given_existing_skill_points(skillTypeID, skillLevel, trainingStartTime, existingSkillPoints)
    506             else:
    507                 skillPointsToTrain, trainingTime = calculator.get_skill_points_and_time_to_train(skillTypeID, skillLevel, trainingStartTime)
    508         except SkillLevelAlreadyTrainedError:
    509             skillPointsToTrain = 0
    510             trainingTime = 0
    511 
    512         return (skillPointsToTrain, float(trainingTime))
    513 
    514     def TrimQueue(self):
    515         trainingTime = 0
    516         skillBoosters = self.GetSkillAcceleratorBoosters()
    517         playerTheoreticalSkillPoints = {}
    518         skills = self.skills.GetSkillsIncludingLapsed()
    519         cutoffIndex = 0
    520         for trainingSkill in self.skillQueue:
    521             queueSkillTypeID = trainingSkill.trainingTypeID
    522             queueSkillLevel = trainingSkill.trainingToLevel
    523             cutoffIndex += 1
    524             addedSP, addedTime, isAccelerated = self.GetAddedSpAndAddedTimeForSkill(queueSkillTypeID, queueSkillLevel, skills, playerTheoreticalSkillPoints, trainingTime, skillBoosters)
    525             trainingTime += addedTime
    526             playerTheoreticalSkillPoints[queueSkillTypeID] += addedSP
    527             if trainingTime > self.GetMaxSkillQueueLimitLength():
    528                 break
    529 
    530         if cutoffIndex < len(self.skillQueue):
    531             removedSkills = self.skillQueue[cutoffIndex:]
    532             self.skillQueue = self.skillQueue[:cutoffIndex]
    533             self.skillQueueCache = None
    534             prompt_player_fsd_dialog('skillQueueTrimmed', {'num': len(removedSkills)})
    535 
    536     def GetSkillAcceleratorBoosters(self):
    537         return self.skills.GetSkillAcceleratorBoosters()
    538 
    539     def GetAddedSpAndAddedTimeForSkill(self, skillTypeID, skillLevel, skillSet, theoreticalSkillPointsDict, trainingTimeOffset, skillBoosters):
    540         skillStartTime = long(trainingTimeOffset) + gametime.GetWallclockTime()
    541         isAccelerated = skillBoosters.is_any_booster_active_at_time(skillStartTime)
    542         if skillTypeID not in theoreticalSkillPointsDict:
    543             skillObj = skillSet.get(skillTypeID, None)
    544             theoreticalSkillPointsDict[skillTypeID] = self.GetSkillPointsFromSkillObject(skillTypeID, skillObj)
    545         addedSP, addedTime = self.GetSkillPointsAndTimeNeededToTrain(skillTypeID, skillLevel, theoreticalSkillPointsDict[skillTypeID], skillStartTime)
    546         return (addedSP, addedTime, isAccelerated)
    547 
    548     def GetAllTrainingLengths(self):
    549         trainingTime = 0
    550         skillBoosters = self.GetSkillAcceleratorBoosters()
    551         resultsDict = {}
    552         playerTheoreticalSkillPoints = {}
    553         skills = self.skills.GetSkillsIncludingLapsed()
    554         for trainingSkill in self.skillQueue:
    555             queueSkillTypeID = trainingSkill.trainingTypeID
    556             queueSkillLevel = trainingSkill.trainingToLevel
    557             addedSP, addedTime, isAccelerated = self.GetAddedSpAndAddedTimeForSkill(queueSkillTypeID, queueSkillLevel, skills, playerTheoreticalSkillPoints, trainingTime, skillBoosters)
    558             trainingTime += addedTime
    559             playerTheoreticalSkillPoints[queueSkillTypeID] += addedSP
    560             resultsDict[queueSkillTypeID, queueSkillLevel] = (trainingTime, addedTime, isAccelerated)
    561 
    562         return resultsDict
    563 
    564     def ApplyFreeSkillPointsToQueue(self):
    565         handler = sm.GetService('skills').GetSkillHandler()
    566         pointsBySkillTypeID = handler.GetFreeSkillPointsAppliedToQueue()
    567         if pointsBySkillTypeID:
    568             handler.ApplyFreeSkillPointsToQueue()
    569 
    570     def ApplyFreeSkillPointsToSkills(self, skillTypeIDsAndLevels):
    571         handler = sm.GetService('skills').GetSkillHandler()
    572         pointsBySkillTypeID = handler.GetFreeSkillPointsAppliedToSkills(skillTypeIDsAndLevels)
    573         if pointsBySkillTypeID:
    574             handler.ApplyFreeSkillPointsToSkills(skillTypeIDsAndLevels)
    575 
    576     def GetSkillLevelAndProgressWithFreePoints(self, pointsBySkillTypeID):
    577         skillSvc = sm.GetService('skills')
    578         levelBySkillTypeID = {}
    579         for typeID, points in pointsBySkillTypeID.iteritems():
    580             currentPoints = skillSvc.MySkillPoints(typeID)
    581             totalPoints = currentPoints + points
    582             rank = skillSvc.GetSkillRank(typeID)
    583             level = GetSkillLevelRaw(totalPoints, rank)
    584             progress = GetLevelProgress(totalPoints, rank)
    585             levelBySkillTypeID[typeID] = (level, progress)
    586 
    587         return levelBySkillTypeID
    588 
    589     def GetProgressOfAllSkills(self):
    590         skillSvc = sm.GetService('skills')
    591         progressBySkillTypeID = {}
    592         for skill in self.GetQueue():
    593             currentPoints = skillSvc.MySkillPoints(skill.trainingTypeID)
    594             rank = skillSvc.GetSkillRank(skill.trainingTypeID)
    595             progress = GetLevelProgress(currentPoints, rank)
    596             progressBySkillTypeID[skill.trainingTypeID] = progress
    597 
    598         return progressBySkillTypeID
    599 
    600     def InternalRemoveFromQueue(self, skillTypeID, skillLevel):
    601         if not len(self.skillQueue):
    602             return
    603         skillPosition = self.FindInQueue(skillTypeID, skillLevel)
    604         if skillPosition is None:
    605             raise UserError('QueueSkillNotPresent')
    606         if skillPosition == len(self.skillQueue):
    607             del self.skillQueueCache[skillTypeID][skillLevel]
    608             self.skillQueue.pop()
    609         else:
    610             self.skillQueueCache = None
    611             self.skillQueue.pop(skillPosition)
    612 
    613     def ClearCache(self):
    614         self.skillQueueCache = None
    615 
    616     def AddToCache(self, skillTypeID, skillLevel, position):
    617         self.PrimeCache()
    618         if skillTypeID not in self.skillQueueCache:
    619             self.skillQueueCache[skillTypeID] = {}
    620         self.skillQueueCache[skillTypeID][skillLevel] = position
    621 
    622     def GetPlayerAttributeDict(self):
    623         return self.skills.GetCharacterAttributes()
    624 
    625     def PrimeCache(self, force = False):
    626         if force:
    627             self.skillQueueCache = None
    628         if self.skillQueueCache is None:
    629             i = 0
    630             self.skillQueueCache = {}
    631             for trainingSkill in self.skillQueue:
    632                 self.AddToCache(trainingSkill.trainingTypeID, trainingSkill.trainingToLevel, i)
    633                 i += 1
    634 
    635     def GetSkillPointsFromSkillObject(self, skillTypeID, skillInfo):
    636         if skillInfo is None:
    637             return 0
    638         totalSkillPoints = skillInfo.trainedSkillPoints or 0
    639         trainingSkill = self.SkillInTraining(skillTypeID)
    640         serverQueue = self.GetServerQueue()
    641         if trainingSkill and len(serverQueue):
    642             skillPointsTrained = self.GetEstimatedSkillPointsTrained(skillTypeID)
    643             totalSkillPoints = max(skillPointsTrained, totalSkillPoints)
    644         return totalSkillPoints
    645 
    646     def GetEstimatedSkillPointsTrained(self, skillTypeID):
    647         startTime = self.GetStartTimeOfQueue()
    648         currentTime = gametime.GetWallclockTime()
    649         if startTime is None:
    650             startTime = currentTime
    651         trainingCalculator = self.skills.GetSkillTrainingTimeCalculator()
    652         with trainingCalculator.specific_current_time_context(startTime):
    653             skillPointsTrained = trainingCalculator.get_skill_points_trained_at_sample_time(skillTypeID, startTime, currentTime)
    654         return skillPointsTrained
    655 
    656     def GetEstimatedTotalSkillPoints(self):
    657         cachedTotalSkillPoints = sm.GetService('skills').GetTotalSkillPointsForCharacter()
    658         currentSkill = self.SkillInTraining()
    659         if currentSkill:
    660             currentSkillPointsForSkill = self.GetSkillPointsFromSkillObject(currentSkill.typeID, currentSkill)
    661             cachedCurrentSkill = sm.GetService('skills').GetSkill(currentSkill.typeID)
    662             if cachedCurrentSkill:
    663                 return cachedTotalSkillPoints - cachedCurrentSkill.trainedSkillPoints + currentSkillPointsForSkill
    664         return cachedTotalSkillPoints
    665 
    666     def OnServerSkillsChanged(self, skillInfos):
    667         self.PrimeCache()
    668         for skillTypeID, skillInfo in skillInfos.iteritems():
    669             skill = self.skills.GetSkill(skillTypeID)
    670             if not skill and skillInfo.trainedSkillLevel > 0:
    671                 self.LogError('skillQueueSvc::OnServerSkillsChanged skill %s not found' % skillTypeID)
    672                 continue
    673             self._RemoveSkillFromQueueCacheIfTrained(skillInfo, skillTypeID)
    674             self._RemoveSkillFromCachedQueueIfTrained(skillInfos, skillTypeID)
    675 
    676     def OnCloseApp(self):
    677         if self.IsQueueChanged():
    678             self.CommitTransaction()
    679 
    680     def _RemoveSkillFromQueueCacheIfTrained(self, skillInfo, skillTypeID):
    681         skillLevel = skillInfo.trainedSkillLevel
    682         if self.skillQueueCache and skillTypeID in self.skillQueueCache:
    683             if skillLevel in self.skillQueueCache[skillTypeID]:
    684                 self.InternalRemoveFromQueue(skillTypeID, skillLevel)
    685 
    686     def _RemoveSkillFromCachedQueueIfTrained(self, skillInfos, skillTypeID):
    687         if self.cachedSkillQueue:
    688             keepSkills = []
    689             for trainingSkill in self.cachedSkillQueue:
    690                 if trainingSkill.trainingTypeID == skillTypeID:
    691                     finishedSkill = skillInfos[skillTypeID]
    692                     if trainingSkill.trainingToLevel <= finishedSkill.trainedSkillLevel:
    693                         continue
    694                 keepSkills.append(trainingSkill)
    695 
    696             self.cachedSkillQueue = keepSkills
    697 
    698     def OnSkillQueuePausedServer(self, *args):
    699         queue = self.skillQueue
    700         if self.IsTransactionOpen():
    701             queue = self.cachedSkillQueue
    702         if queue:
    703             for skillEntry in queue:
    704                 skillEntry.trainingStartTime = skillEntry.trainingEndTime = None
    705 
    706         sm.ScatterEvent('OnSkillQueuePaused')
    707         PlaySound('skill_training_stop_play')
    708 
    709     def OnNewSkillQueueSaved(self, newQueue):
    710         if self.IsTransactionOpen():
    711             self.cachedSkillQueue = newQueue
    712         self.skillQueue = newQueue
    713         sm.ScatterEvent('OnSkillQueueChanged')
    714 
    715     def TrainSkillNow(self, skillTypeID, toSkillLevel, *args):
    716         inTraining = self.SkillInTraining()
    717         if inTraining and prompt_player_fsd_dialog('ConfirmSkillTrainingNow', {'name': evetypes.GetName(inTraining.typeID),
    718          'lvl': inTraining.trainedSkillLevel + 1}, uiconst.OKCANCEL) != uiconst.ID_OK:
    719             return
    720         self.BeginTransaction()
    721         try:
    722             if self.FindInQueue(skillTypeID, toSkillLevel) is not None:
    723                 self.MoveSkillToPosition(skillTypeID, toSkillLevel, 0)
    724                 message = ('SkillQueueStarted',)
    725             else:
    726                 self.AddSkillToQueue(skillTypeID, toSkillLevel, 0)
    727                 skillName = localization.GetByLabel('UI/SkillQueue/Skills/SkillNameAndLevel', skill=skillTypeID, amount=toSkillLevel)
    728                 if inTraining:
    729                     message = ('AddedToQueue', {'skillname': skillName})
    730                 else:
    731                     message = ('AddedToQueueAndStarted', {'skillname': skillName})
    732             self.CommitTransaction()
    733             prompt_player_fsd_dialog(*message)
    734         except Exception:
    735             self.RollbackTransaction()
    736             raise
    737 
    738     def AddSkillToEnd(self, skillTypeID, current, nextLevel = None):
    739         queueLength = self.GetNumberOfSkillsInQueue()
    740         if queueLength >= SKILLQUEUE_MAX_NUM_SKILLS:
    741             raise UserError('QueueTooManySkills', {'num': SKILLQUEUE_MAX_NUM_SKILLS})
    742         totalTime, _ = self.GetTrainingLengthOfQueue()
    743         if totalTime > self.GetMaxSkillQueueLimitLength():
    744             raise UserError('QueueTooLong')
    745         if nextLevel is None:
    746             queue = self.GetServerQueue()
    747             nextLevel = self.FindNextLevel(skillTypeID, current, queue)
    748         if self.FindInQueue(skillTypeID, nextLevel) is not None:
    749             raise UserError('QueueSkillAlreadyPresent')
    750         self.BeginTransaction()
    751         try:
    752             self.AddSkillToQueue(skillTypeID, nextLevel)
    753             self.CommitTransaction()
    754         except Exception:
    755             self.RollbackTransaction()
    756             raise
    757 
    758         text = localization.GetByLabel('UI/SkillQueue/Skills/SkillNameAndLevel', skill=skillTypeID, amount=nextLevel)
    759         if self.SkillInTraining():
    760             prompt_player_fsd_dialog('AddedToQueue', {'skillname': text})
    761         else:
    762             prompt_player_fsd_dialog('AddedToQueueAndStarted', {'skillname': text})
    763         sm.ScatterEvent('OnSkillQueueRefreshed')
    764 
    765     def FindNextLevel(self, skillTypeID, current = None, skillQueue = None):
    766         skill = self.skills.GetSkillIncludingLapsed(skillTypeID)
    767         current = skill.trainedSkillLevel or 0 if skill else 0
    768         if skillQueue is None:
    769             skillQueue = self.GetServerQueue()
    770         skillQueue = [ (skill.trainingTypeID, skill.trainingToLevel) for skill in skillQueue ]
    771         nextLevel = None
    772         for i in xrange(1, 7):
    773             if i <= current:
    774                 continue
    775             inQueue = bool((skillTypeID, i) in skillQueue)
    776             if inQueue is False:
    777                 nextLevel = i
    778                 break
    779 
    780         return nextLevel
    781 
    782     def OnMultipleCharactersTrainingUpdated(self):
    783         self.PrimeCache(True)
    784         self._isAllCharacterTrainingSlotsUsed = None
    785         sm.ScatterEvent('OnMultipleCharactersTrainingRefreshed')
    786 
    787     def GetMultipleCharacterTraining(self, force = False):
    788         userSvc = sm.RemoteSvc('userSvc')
    789         if force:
    790             userSvc.InvalidateMultiCharacterTrainingCache(session.userid)
    791         return userSvc.GetMultiCharactersTrainingSlots()
    792 
    793     def IsSkillInQueue(self, skillTypeID):
    794         self.PrimeCache()
    795         if skillTypeID in self.skillQueueCache:
    796             return True
    797         return False
    798 
    799     def IsAllCharacterTrainingSlotsUsed(self):
    800         if self._isAllCharacterTrainingSlotsUsed is None:
    801             queues = self.GetMultipleCharacterTraining().items()
    802             characterData = sm.GetService('cc').GetCharacterSelectionData()
    803             activeQueues = 1 + len(queues)
    804             usedQueues = 0
    805             for characterDetails in characterData.details.values():
    806                 isTraining = characterDetails.GetSkillInTrainingInfo()['currentSkill'] is not None
    807                 if characterDetails.charID != session.charid and isTraining:
    808                     usedQueues += 1
    809 
    810             self._isAllCharacterTrainingSlotsUsed = usedQueues == activeQueues
    811         return self._isAllCharacterTrainingSlotsUsed
    812 
    813     def IsQueueWndOpen(self):
    814         from eve.client.script.ui.shared.neocom.characterSheetWindow import CharacterSheetWindow
    815         return CharacterSheetWindow.IsOpen()
    816 
    817     def FindHighestLevelInQueue(self, skillTypeID):
    818         if self.IsSkillInQueue(skillTypeID):
    819             return max(self.skillQueueCache[skillTypeID])
    820 
    821     def RemoveHighestLevelFromQueue(self, skillTypeID):
    822         if self.IsSkillInQueue(skillTypeID):
    823             self.RemoveSkillFromQueue(skillTypeID, self.FindHighestLevelInQueue(skillTypeID))
    824 
    825     def GetAddMenuForSkillEntries(self, skillTypeID, skillInfo):
    826         m = []
    827         if skillInfo is None:
    828             return m
    829         skillLevel = skillInfo.trainedSkillLevel
    830         if skillLevel is not None:
    831             if skillLevel < skillConst.skill_max_level:
    832                 queue = self.GetQueue()
    833                 nextLevel = self.FindNextLevel(skillTypeID, skillLevel, queue)
    834                 if not self.SkillInTraining(skillTypeID):
    835                     trainingTime, totalTime, _ = self.GetTrainingLengthOfSkill(skillTypeID, skillLevel + 1, 0)
    836                     if trainingTime <= 0:
    837                         takesText = localization.GetByLabel('UI/SkillQueue/Skills/CompletionImminent')
    838                     else:
    839                         takesText = localization.GetByLabel('UI/SkillQueue/Skills/SkillTimeLeft', timeLeft=long(trainingTime))
    840                     if nextLevel < 6 and self.FindInQueue(skillTypeID, skillLevel + 1) is None:
    841                         trainText = MenuLabel('UI/SkillQueue/AddSkillMenu/AddToFrontOfQueueTime', {'takes': takesText})
    842                         m.append((trainText, self.AddSkillToQueue, (skillTypeID, nextLevel, 0)))
    843                 if self.IsSkillInQueue(skillTypeID):
    844                     removeText = MenuLabel('UI/SkillQueue/AddSkillMenu/RemoveHighestLevelFromQueue', {'level': self.FindHighestLevelInQueue(skillTypeID)})
    845                     m.append((removeText, self.RemoveHighestLevelFromQueue, (skillTypeID,)))
    846                 if nextLevel < 6:
    847                     label = MenuLabel('UI/SkillQueue/AddSkillMenu/AddToEndOfQueue', {'nextLevel': nextLevel})
    848                     m.append((label, self.AddSkillToQueue, (skillInfo.typeID,)))
    849                     label = MenuLabel('UI/SkillQueue/AddSkillMenu/TrainToLevel5', {'nextLevel': nextLevel})
    850                     m.append((label, self.AddAllLevels, (skillInfo.typeID, nextLevel)))
    851                 if sm.GetService('skills').GetFreeSkillPoints() > 0:
    852                     diff = sm.GetService('skills').SkillpointsNextLevel(skillTypeID) + 0.5 - skillInfo.trainedSkillPoints
    853                     m.append((MenuLabel('UI/SkillQueue/AddSkillMenu/ApplySkillPoints'), self.UseFreeSkillPoints, (skillInfo.typeID, diff)))
    854         if m:
    855             m.append(None)
    856         return m
    857 
    858     def AddAllLevels(self, typeID, nextLevel):
    859         skills = [ (typeID, lvl) for lvl in xrange(nextLevel, 6) ]
    860         self.AddSkillsToQueue(skills, ignoreAlreadyPresent=True)
    861 
    862     def UseFreeSkillPoints(self, skillTypeID, diff):
    863         inTraining = self.SkillInTraining()
    864         if inTraining is not None and inTraining.typeID == skillTypeID:
    865             prompt_player_fsd_dialog('CannotApplyFreePointsWhileTrainingSkill')
    866             return
    867         freeSkillPoints = sm.StartService('skills').GetFreeSkillPoints()
    868         text = localization.GetByLabel('UI/SkillQueue/AddSkillMenu/UseSkillPointsWindow', skill=skillTypeID, skillPoints=int(diff))
    869         caption = localization.GetByLabel('UI/SkillQueue/AddSkillMenu/ApplySkillPoints')
    870         ret = uix.QtyPopup(maxvalue=freeSkillPoints, caption=caption, label=text, setvalue=int(diff))
    871         if ret is None:
    872             return
    873         sp = int(ret.get('qty', ''))
    874         sm.StartService('skills').ApplyFreeSkillPoints(skillTypeID, sp)
    875         PlaySound('st_allocate_skillpoints_play')
    876 
    877     def SkillInTraining(self, skillTypeID = None):
    878         activeQueue = self.GetServerQueue()
    879         if len(activeQueue) and activeQueue[0].trainingEndTime:
    880             if skillTypeID is None or activeQueue[0].trainingTypeID == skillTypeID:
    881                 return self.skills.GetSkill(activeQueue[0].trainingTypeID)
    882 
    883     def GetActiveSkillTypeID(self):
    884         skill = self.SkillInTraining()
    885         if skill:
    886             return skill.typeID
    887 
    888     def GetTimeForTraining(self, skillTypeID, toLevel, trainingStartTime = 0):
    889         currentTraining = self.SkillInTraining(skillTypeID)
    890         currentSkillPointsDict = {}
    891         currentTime = gametime.GetWallclockTime()
    892         if currentTraining:
    893             trainingEndTime = self.GetEndOfTraining(skillTypeID)
    894             timeForTraining = trainingEndTime - currentTime
    895         else:
    896             timeOffset = 0
    897             if trainingStartTime:
    898                 timeOffset = trainingStartTime - currentTime
    899             skill = self.skills.GetSkill(skillTypeID)
    900             attributes = self.GetPlayerAttributeDict()
    901             skillBoosters = self.GetSkillAcceleratorBoosters()
    902             skillBoosters.apply_expired_attributes_at_time_offset(attributes, timeOffset)
    903             currentSkillPointsDict[skillTypeID] = self.GetSkillPointsFromSkillObject(skillTypeID, skill)
    904             _, timeForTraining = self.GetSkillPointsAndTimeNeededToTrain(skillTypeID, toLevel, currentSkillPointsDict[skillTypeID], trainingStartTime or currentTime)
    905         return long(timeForTraining)
    906 
    907     def GetEndOfTraining(self, skillTypeID):
    908         skillQueue = self.GetServerQueue()
    909         if not len(skillQueue) or skillQueue[0].trainingTypeID != skillTypeID:
    910             return None
    911         else:
    912             return skillQueue[0].trainingEndTime
    913 
    914     def GetStartTimeOfQueue(self):
    915         skillQueue = self.GetServerQueue()
    916         if not skillQueue:
    917             return None
    918         else:
    919             return skillQueue[0].trainingStartTime
    920 
    921     def IsMoveAllowed(self, draggedNode, checkedIdx):
    922         queue = self.GetQueue()
    923         if checkedIdx is None:
    924             checkedIdx = len(queue)
    925         guid = getattr(draggedNode, '__guid__', None)
    926         skillSvc = sm.GetService('skills')
    927         if getattr(draggedNode, 'skillID', None):
    928             if guid == 'listentry.SkillEntry':
    929                 level = self.FindNextLevel(draggedNode.skillID, None, queue)
    930             else:
    931                 level = draggedNode.Get('trainToLevel', 1)
    932                 if draggedNode.inQueue is None:
    933                     level += 1
    934             return self.CheckCanInsertSkillAtPosition(draggedNode.skillID, level, checkedIdx, check=1, performLengthTest=False)
    935         if guid in ('xtriui.InvItem', 'listentry.InvItem'):
    936             category = GetAttrs(draggedNode, 'rec', 'categoryID')
    937             if category != const.categorySkill:
    938                 return
    939             typeID = GetAttrs(draggedNode, 'rec', 'typeID')
    940             if typeID is None:
    941                 return
    942             if skillSvc.IsSkillInjected(typeID):
    943                 return False
    944             meetsReq = sm.StartService('godma').CheckSkillRequirementsForType(typeID)
    945             if not meetsReq:
    946                 return False
    947             return True
    948         if guid == 'listentry.SkillTreeEntry':
    949             typeID = draggedNode.typeID
    950             if typeID is None:
    951                 return False
    952             if not skillSvc.IsSkillInjected(typeID):
    953                 return False
    954             skill = skillSvc.GetSkill(typeID)
    955             level = self.FindNextLevel(typeID, skill.trainedSkillLevel, queue)
    956             return self.CheckCanInsertSkillAtPosition(typeID, level, checkedIdx, check=1, performLengthTest=False)
    957         if getattr(draggedNode, 'typeID', None):
    958             if evetypes.GetCategoryID(draggedNode.typeID) == const.categorySkill:
    959                 level = self.FindNextLevel(draggedNode.typeID, None, queue)
    960                 return self.CheckCanInsertSkillAtPosition(draggedNode.typeID, level, checkedIdx, check=1, performLengthTest=False)
    961 
    962     def IsSkillEntry(self, draggedNode):
    963         if getattr(draggedNode, 'skillID', None):
    964             return True
    965         guid = getattr(draggedNode, '__guid__', None)
    966         if guid in ('xtriui.InvItem', 'listentry.InvItem'):
    967             category = GetAttrs(draggedNode, 'rec', 'categoryID')
    968             if category == const.categorySkill:
    969                 return True
    970         else:
    971             if guid == 'listentry.SkillTreeEntry':
    972                 return True
    973             if getattr(draggedNode, 'typeID', None):
    974                 if evetypes.GetCategoryID(draggedNode.typeID) == const.categorySkill:
    975                     return True
    976         return False
    977 
    978     def IsRemoveAllowed(self, typeID, level):
    979         try:
    980             self.CheckCanRemoveSkillFromQueue(typeID, level)
    981             return True
    982         except UserError:
    983             return False
    984 
    985     def GetSkillPlanImporter(self):
    986         if self.skillplanImporter is None:
    987             self.skillplanImporter = ImportSkillPlan(IsUsingDefaultLanguage(session))
    988         return self.skillplanImporter
    989 
    990     @contextmanager
    991     def ChangeQueueLock(self):
    992         ReentrantLock(self, 'SkillQueueSvc:xActLock')
    993         try:
    994             yield
    995         finally:
    996             UnLock(self, 'SkillQueueSvc:xActLock')
    997 
    998     def IsQueueChanged(self):
    999         if self.cachedSkillQueue is None:
   1000             return False
   1001         if len(self.skillQueue) != len(self.cachedSkillQueue):
   1002             return True
   1003         for s1, s2 in zip(self.skillQueue, self.cachedSkillQueue):
   1004             isSameType = s1.trainingTypeID == s2.trainingTypeID
   1005             isSameLevel = s1.trainingToLevel == s2.trainingToLevel
   1006             if not isSameType or not isSameLevel:
   1007                 return True
   1008 
   1009         return False
   1010 
   1011     def GetTimeSavedByUnallocatedPoints(self, unallocatedPoints):
   1012         totalTimeSaved = 0
   1013         allTrainingLengths = self.GetAllTrainingLengths()
   1014         skillInTraining = self.SkillInTraining()
   1015         actualEnd = self.GetEndOfTraining(skillInTraining.typeID) if skillInTraining else None
   1016         for skill in self.GetQueue():
   1017             trainingID = (skill.trainingTypeID, skill.trainingToLevel)
   1018             if trainingID not in allTrainingLengths:
   1019                 continue
   1020             timeLeft = allTrainingLengths[trainingID][1]
   1021             if skillInTraining and skill.trainingTypeID == skillInTraining.typeID and skill.trainingToLevel - 1 == skillInTraining.trainedSkillLevel:
   1022                 if actualEnd is not None:
   1023                     timeLeft = float(actualEnd - blue.os.GetWallclockTime())
   1024             if unallocatedPoints > 0:
   1025                 pointsTrained = skill.trainingDestinationSP - skill.trainingStartSP
   1026                 if unallocatedPoints >= pointsTrained:
   1027                     totalTimeSaved += timeLeft
   1028                 else:
   1029                     fractionCovered = min(1.0, unallocatedPoints / float(pointsTrained))
   1030                     totalTimeSaved += fractionCovered * timeLeft
   1031                 unallocatedPoints -= pointsTrained
   1032 
   1033         return totalTimeSaved