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

autopilot.py (33223B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\parklife\autopilot.py
      2 import itertools
      3 import sys
      4 from collections import defaultdict
      5 import utillib
      6 from carbon.common.script.sys.service import Service
      7 from carbon.common.script.util.timerstuff import AutoTimer
      8 from eve.common.lib import appConst as const
      9 from eve.client.script.ui.services.menuSvcExtras import movementFunctions
     10 import destiny
     11 import uthread
     12 import blue
     13 import log
     14 import localization
     15 import geo2
     16 from carbonui import uiconst
     17 from eve.client.script.ui.services.menuSvcExtras.movementFunctions import WarpToItem
     18 from carbonui.uicore import uicore
     19 from eve.common.script.sys import eveCfg, idCheckers
     20 from eve.common.script.sys.idCheckers import IsTriglavianSystem
     21 from eveexceptions import ExceptionEater, UserError
     22 from eveservices.menu import GetMenuService
     23 from player_messaging.client.ui_message import prompt_player_fsd_dialog
     24 from structures import SERVICE_JUMP_BRIDGE
     25 from stargate.client.jump_checks import check_cancel_stargate_jump
     26 AUTO_NAVIGATION_LOOP_INTERVAL_MS = 2000
     27 
     28 class AutoPilot(Service):
     29     __guid__ = 'svc.autoPilot'
     30     __exportedcalls__ = {'SetOn': [],
     31      'SetOff': [],
     32      'GetState': []}
     33     __notifyevents__ = ['OnBallparkCall',
     34      'OnSessionChanged',
     35      'OnRemoteMessage',
     36      'OnPlayerPodRespawn',
     37      'OnCharacterSessionChanged',
     38      'OnSessionReset',
     39      'OnMapShowsJumpGatesChanged']
     40     __dependencies__ = ['michelle',
     41      'starmap',
     42      'clientPathfinderService',
     43      'structureDirectory']
     44 
     45     def __init__(self):
     46         Service.__init__(self)
     47         self.updateTimer = None
     48         self.updateJumpGatesTimer = None
     49         self.autopilot = 0
     50         self.ignoreTimerCycles = 0
     51         self.isOptimizing = False
     52         self.__navigateSystemDestinationItemID = None
     53         self.__navigateSystemThread = None
     54         self.enabledTimestamp = 0
     55         uthread.new(self.UpdateWaypointsThread).context = 'autoPilot::UpdateWaypointsThread'
     56 
     57     def UpdateWaypointsThread(self):
     58         blue.pyos.synchro.SleepWallclock(2000)
     59         starmapSvc = sm.GetService('starmap')
     60         waypoints = starmapSvc.GetWaypoints()
     61         if len(waypoints):
     62             loopCounter = 0
     63             while loopCounter < 5 and (not sm.IsServiceRunning('clientPathfinderService') or not sm.GetService('clientPathfinderService').initialized):
     64                 loopCounter += 1
     65                 blue.pyos.synchro.SleepWallclock(1000)
     66 
     67             starmapSvc.SetWaypoints(waypoints)
     68 
     69     def Run(self, memStream = None):
     70         Service.Run(self, memStream)
     71         self.StartTimer()
     72 
     73     def DoLogAutopilotEvent(self, columnNames, *args):
     74         if sm.GetService('machoNet').GetGlobalConfig().get('disableAutopilotLogging', 0):
     75             return
     76         try:
     77             sm.ProxySvc('eventLog').LogClientEvent('autopilot', columnNames, *args)
     78         except UserError:
     79             pass
     80 
     81     def SetOn(self):
     82         if self.autopilot == 1:
     83             return
     84         if not eveCfg.InShip():
     85             return
     86         self.autopilot = 1
     87         self.CancelSystemNavigation()
     88         sm.ScatterEvent('OnAutoPilotOn')
     89         prompt_player_fsd_dialog('AutoPilotEnabled')
     90         self.KillTimer()
     91         self.StartTimer()
     92         self.LogNotice('Autopilot Enabled')
     93         self.enabledTimestamp = blue.os.GetWallclockTimeNow()
     94         with ExceptionEater('eventLog'):
     95             starmapSvc = sm.GetService('starmap')
     96             waypoints = starmapSvc.GetWaypoints()
     97             destinationID = waypoints[-1] if waypoints else None
     98             routeData = starmapSvc.GetAutopilotRoute()
     99             routeData = routeData or []
    100             solarSystemIDs = ','.join([ str(s) for s in routeData ])
    101             routeType = starmapSvc.GetRouteType()
    102             avoidPodKill = settings.char.ui.Get('pfAvoidPodKill', 0)
    103             avoidTriglavianTales = settings.char.ui.Get('pfAvoidTriglavianTales', 0)
    104             stopAtEachWaypoint = settings.user.ui.Get('autopilot_stop_at_each_waypoint', False)
    105             avoidSystems = settings.char.ui.Get('pfAvoidSystems', True)
    106             avoidanceItems = self.clientPathfinderService.GetAvoidanceItems()
    107             mandatoryAvoidanceItems = self.clientPathfinderService.GetMandatoryAvoidanceItems()
    108             securityPenalty = settings.char.ui.Get('pfPenalty', 50.0)
    109             uthread.new(self.DoLogAutopilotEvent, ['numWaypoints',
    110              'numJumps',
    111              'routeType',
    112              'avoidSystems',
    113              'avoidanceItems',
    114              'mandatoryAvoidanceItems',
    115              'securityPenalty',
    116              'avoidPodKill',
    117              'avoidTriglavianTales',
    118              'stopAtEachWaypoint',
    119              'destinationID',
    120              'celestialID...'], 'Enabled', len(waypoints), len(routeData), routeType, avoidSystems, ','.join([ str(i) for i in avoidanceItems ]), ','.join([ str(i) for i in mandatoryAvoidanceItems ]), securityPenalty, avoidPodKill, avoidTriglavianTales, stopAtEachWaypoint, destinationID, solarSystemIDs)
    121 
    122     def OnSessionChanged(self, isremote, session, change):
    123         self.KillTimer()
    124         self.ignoreTimerCycles = 3
    125         self.StartTimer()
    126         sm.GetService('starmap').UpdateRoute(fakeUpdate=True)
    127 
    128     def SetOff(self, reason = ''):
    129         if self.autopilot == 0:
    130             self.KillTimer()
    131             return
    132         sm.ScatterEvent('OnAutoPilotOff')
    133         self.autopilot = 0
    134         if reason == '  - waypoint reached':
    135             prompt_player_fsd_dialog('AutoPilotWaypointReached')
    136         elif reason == '  - no destination path set':
    137             prompt_player_fsd_dialog('AutoPilotDisabledNoPathSet')
    138         else:
    139             prompt_player_fsd_dialog('AutoPilotDisabled')
    140         self.LogNotice('Autopilot Disabled', reason)
    141         with ExceptionEater('eventLog'):
    142             starmapSvc = sm.GetService('starmap')
    143             numSeconds = (blue.os.GetWallclockTimeNow() - self.enabledTimestamp) / const.SEC
    144             uthread.new(self.DoLogAutopilotEvent, ['secondsEnabled', 'reason'], 'Disabled', numSeconds, reason)
    145         self.enabledTimestamp = 0
    146 
    147     def OnRemoteMessage(self, msgID, *args, **kwargs):
    148         if msgID == 'FleetWarp':
    149             self.LogInfo('Canceling auto navigation due to fleet warp detected')
    150             self.CancelSystemNavigation()
    151 
    152     def OnPlayerPodRespawn(self):
    153         self.CancelSystemNavigation()
    154 
    155     def OnBallparkCall(self, functionName, args):
    156         functions = ['GotoDirection', 'GotoPoint']
    157         if args[0] != session.shipid:
    158             return
    159         cancelAutoNavigation = False
    160         if self.__navigateSystemDestinationItemID is None:
    161             pass
    162         elif functionName in {'GotoDirection', 'GotoPoint', 'Orbit'}:
    163             cancelAutoNavigation = True
    164         elif functionName == 'FollowBall' and self.__navigateSystemDestinationItemID != args[1]:
    165             cancelAutoNavigation = True
    166         elif functionName == 'WarpTo':
    167             bp = sm.GetService('michelle').GetBallpark()
    168             autoNavigationBall = bp.GetBall(self.__navigateSystemDestinationItemID)
    169             if autoNavigationBall:
    170                 navDstVector = (autoNavigationBall.x, autoNavigationBall.y, autoNavigationBall.z)
    171                 dstVector = args[1:-2]
    172                 if navDstVector and dstVector and geo2.Vec3DistanceD(navDstVector, dstVector) - autoNavigationBall.radius > const.minWarpDistance:
    173                     cancelAutoNavigation = True
    174         if cancelAutoNavigation:
    175             self.LogInfo('Canceling auto navigation to', self.__navigateSystemDestinationItemID, 'as a respons to OnBallparkCall:', functionName, args)
    176             self.CancelSystemNavigation()
    177         if functionName in functions:
    178             if functionName == 'GotoDirection' and self.gotoCount > 0:
    179                 self.gotoCount = 0
    180                 self.LogInfo('Autopilot gotocount set to 0')
    181                 return
    182             if self.gotoCount == 0:
    183                 waypoints = sm.GetService('starmap').GetWaypoints()
    184                 if waypoints and self.IsStationOrStructure(waypoints[-1]):
    185                     return
    186             self.SetOff(functionName + str(args))
    187             self.LogInfo('Autopilot stopped gotocount is ', self.gotoCount)
    188 
    189     def GetState(self):
    190         return self.autopilot
    191 
    192     def Stop(self, stream):
    193         self.KillTimer()
    194         Service.Stop(self)
    195 
    196     def KillTimer(self):
    197         self.updateTimer = None
    198 
    199     def StartTimer(self):
    200         self.gotoCount = 0
    201         self.updateTimer = AutoTimer(2000, self.Update)
    202         self.updateJumpGatesTimer = AutoTimer(const.HOUR / const.MSEC, self.UpdateJumpGatesTimerThread)
    203 
    204     def IsStationOrStructure(self, itemID):
    205         if idCheckers.IsSolarSystem(itemID):
    206             return False
    207         return idCheckers.IsStation(itemID) or self.structureDirectory.GetStructureInfo(itemID) is not None
    208 
    209     def GetGateOrStationID(self, bp, destinationID):
    210         if idCheckers.IsSolarSystem(destinationID):
    211             gateID = self._GetGateIDToSolarsystem(destinationID)
    212             if gateID:
    213                 return gateID
    214         if idCheckers.IsStation(destinationID):
    215             return destinationID
    216         slimItem = bp.GetInvItem(destinationID)
    217         if slimItem and slimItem.categoryID == const.categoryStructure:
    218             return destinationID
    219         return self.GetGateOrStation(bp, destinationID)[0]
    220 
    221     def _GetGateIDToSolarsystem(self, destinationID):
    222         fromLocationID = session.solarsystemid2
    223         return self.GetGateIDToSolarsystemFromLocationID(destinationID, fromLocationID)
    224 
    225     def GetGateIDToSolarsystemFromLocationID(self, destinationID, fromLocationID, addWarning = True):
    226         try:
    227             destGateIds = cfg.mapSolarSystemContentCache[destinationID].stargates.keys()
    228             for sgid, sg in cfg.mapSolarSystemContentCache[fromLocationID].stargates.iteritems():
    229                 if sg.destination in destGateIds:
    230                     return sgid
    231 
    232             if addWarning:
    233                 self.LogWarn('Autopilot: _GetGateIDToSolarsystem, did not find gate for destination', destinationID, fromLocationID)
    234         except Exception:
    235             self.LogException()
    236 
    237     def GetGateOrStation(self, bp, destinationID, addWarning = True):
    238         for solarsystemItem in sm.GetService('map').GetSolarsystemItems(session.solarsystemid2, False):
    239             ballID = solarsystemItem.itemID
    240             slimItem = bp.GetInvItem(ballID)
    241             if slimItem is None:
    242                 continue
    243             if slimItem.groupID == const.groupStargate and destinationID in map(lambda x: x.locationID, slimItem.jumps):
    244                 return (ballID, slimItem)
    245             if slimItem.groupID == const.groupUpwellJumpGate:
    246                 if destinationID == getattr(slimItem, 'targetSolarsystemID', None):
    247                     return (ballID, slimItem)
    248             elif destinationID == slimItem.itemID:
    249                 return (ballID, slimItem)
    250 
    251         if addWarning:
    252             self.LogWarn('Autopilot: GetGateOrStation, did not find destination', destinationID, session.solarsystemid2)
    253         return (None, None)
    254 
    255     def GetDestinationItemID(self):
    256         destinationPath = sm.GetService('starmap').GetDestinationPath()
    257         if len(destinationPath) > 0:
    258             nextItemInRoute = destinationPath[0]
    259             ballpark = sm.GetService('michelle').GetBallpark()
    260             if ballpark and session.solarsystemid and ballpark.solarsystemID == session.solarsystemid:
    261                 destinationID, _ = self.GetGateOrStation(ballpark, nextItemInRoute, addWarning=False)
    262                 return destinationID
    263 
    264     def Update(self):
    265         if self.autopilot == 0:
    266             self.KillTimer()
    267             return
    268         if self.ignoreTimerCycles > 0:
    269             self.ignoreTimerCycles -= 1
    270             return
    271         if not session.IsItSafe():
    272             self.LogInfo('returning as it is not safe')
    273             return
    274         if not session.rwlock.IsCool():
    275             self.LogInfo("returning as the session rwlock isn't cool")
    276             return
    277         if not eveCfg.InShip():
    278             return
    279         starmapSvc = sm.GetService('starmap')
    280         destinationPath = starmapSvc.GetDestinationPath()
    281         if len(destinationPath) == 0:
    282             self.SetOff('  - no destination path set')
    283             return
    284         if destinationPath[0] is None:
    285             if not starmapSvc.GetWaypoints():
    286                 self.SetOff('  - no destination path set')
    287             else:
    288                 self.SetOff('  - destination not in expected system.')
    289             return
    290         bp = sm.GetService('michelle').GetBallpark()
    291         if not bp:
    292             return
    293         if sm.GetService('jumpQueue').IsJumpQueued():
    294             return
    295         ship = bp.GetBall(session.shipid)
    296         if ship is None:
    297             return
    298         if ship.mode == destiny.DSTBALL_WARP:
    299             return
    300         if bp.solarsystemID != session.solarsystemid:
    301             self.LogError('autopilot - waiting a bit as we have not updated the ballpark', session.solarsystemid, bp.solarsystemID)
    302             return
    303         destID, destItem = self.GetGateOrStation(bp, destinationPath[0])
    304         if destID is None:
    305             if len(destinationPath) == 1:
    306                 self.SetOff('  - destination not in expected system.')
    307             return
    308         jumpingToCelestial = not idCheckers.IsSolarSystem(destinationPath[0])
    309         theJump = None
    310         if not jumpingToCelestial:
    311             for jump in destItem.jumps:
    312                 if destinationPath[0] == jump.locationID:
    313                     theJump = jump
    314                     break
    315 
    316             if destinationPath[0] and destinationPath[0] == getattr(destItem, 'targetSolarsystemID', None):
    317                 theJump = utillib.KeyVal(locationID=destItem.targetSolarsystemID, toCelestialID='-1')
    318         if theJump is None and not jumpingToCelestial:
    319             return
    320         approachObject = bp.GetBall(destID)
    321         if approachObject is None:
    322             return
    323         if jumpingToCelestial:
    324             jumpToLocationName = cfg.evelocations.Get(destinationPath[0]).name
    325         else:
    326             jumpToLocationName = cfg.evelocations.Get(theJump.locationID).name
    327         shipDestDistance = bp.GetSurfaceDist(ship.id, destID)
    328         if shipDestDistance < const.maxStargateJumpingDistance and not jumpingToCelestial:
    329             if not self._IsSessionAndBpSafeToJump(bp, ship):
    330                 return
    331             try:
    332                 self.LogNotice('Autopilot jumping from', destID, 'to', theJump.toCelestialID, '(', jumpToLocationName, ')')
    333                 if not jumpingToCelestial:
    334                     if check_cancel_stargate_jump(destID, theJump.locationID, exclude_checks=['illicit_goods']):
    335                         self.SetOff()
    336                         return
    337                 if destItem.groupID == const.groupUpwellJumpGate:
    338                     didJumpThrough = sm.GetService('sessionMgr').PerformSessionChange('autopilot', sm.RemoteSvc('structureJumpBridgeMgr').CmdJumpThroughStructureStargate, destID)
    339                     if not didJumpThrough:
    340                         self.SetOff()
    341                         return
    342                 else:
    343                     gateRestricted = sm.GetService('gatejump').CheckForGateRestriction(session.solarsystemid, destID)
    344                     if gateRestricted:
    345                         self.SetOff('  - Gate Restricted')
    346                         return
    347                     sm.GetService('sessionMgr').PerformSessionChange('autopilot', sm.GetService('michelle').GetRemotePark().CmdStargateJump, destID, theJump.toCelestialID, session.shipid)
    348                 prompt_player_fsd_dialog('AutoPilotJumping', {'what': jumpToLocationName})
    349                 sm.ScatterEvent('OnAutoPilotJump')
    350                 if IsTriglavianSystem(theJump.locationID):
    351                     self.ignoreTimerCycles = 13
    352                 else:
    353                     self.ignoreTimerCycles = 5
    354             except UserError as e:
    355                 self._HandleJumpUserErrors(e, destID, jumpToLocationName)
    356             except:
    357                 sys.exc_clear()
    358                 self.LogError('Autopilot: jumping to ' + jumpToLocationName + ' failed. Will try again')
    359                 self.ignoreTimerCycles = 5
    360 
    361             return
    362         if shipDestDistance < const.maxStargateJumpingDistance and destItem.groupID == const.groupUpwellJumpGate:
    363             if not self._IsSessionAndBpSafeToJump(bp, ship):
    364                 return
    365             try:
    366                 didJumpThrough = sm.GetService('menu').ReallyJumpThroughStructureJumpBridge(destID)
    367                 self.ignoreTimerCycles = 5
    368                 if didJumpThrough:
    369                     prompt_player_fsd_dialog('AutoPilotJumping', {'what': jumpToLocationName})
    370                     sm.ScatterEvent('OnAutoPilotJump')
    371                 else:
    372                     self.SetOff()
    373             except UserError as e:
    374                 self._HandleJumpUserErrors(e, destID, jumpToLocationName)
    375             except:
    376                 sys.exc_clear()
    377                 self.LogError('Autopilot: jumping to ' + jumpToLocationName + ' failed. Will try again')
    378                 self.ignoreTimerCycles = 5
    379 
    380             return
    381         if jumpingToCelestial and shipDestDistance < const.maxDockingDistance and self.IsStationOrStructure(destID):
    382             if self.__navigateSystemDestinationItemID != destID:
    383                 if shipDestDistance > 2500:
    384                     sm.GetService('audio').AudioMessage('msg_AutoPilotApproachingStation_play')
    385                 GetMenuService().Dock(destID)
    386                 self.ignoreTimerCycles = 5
    387         elif shipDestDistance < const.minWarpDistance:
    388             if ship.mode == destiny.DSTBALL_FOLLOW and ship.followId == destID:
    389                 return
    390             self.AP_ApproachDestID(destID, jumpingToCelestial)
    391         else:
    392             self.AP_WarpToDestID(destID, jumpToLocationName, jumpingToCelestial)
    393 
    394     def _IsSessionAndBpSafeToJump(self, bp, ship):
    395         if ship.isCloaked:
    396             return False
    397         if session.mutating:
    398             self.LogInfo('session is mutating')
    399             return False
    400         if session.changing:
    401             self.LogInfo('session is changing')
    402             return False
    403         if bp.solarsystemID != session.solarsystemid:
    404             self.LogInfo('bp.solarsystemid is not solarsystemid')
    405             return False
    406         if sm.GetService('michelle').GetRemotePark()._Moniker__bindParams != session.solarsystemid:
    407             self.LogInfo('remote park moniker bindparams is not solarsystemid')
    408             return False
    409         return True
    410 
    411     def _HandleJumpUserErrors(self, e, destID, jumpToLocationName):
    412         if e.msg in ('SystemCheck_JumpFailed_Stuck', 'CantJumpEmanationLockedToAnotherGate'):
    413             self.SetOff()
    414             raise
    415         elif e.msg.startswith('SystemCheck_JumpFailed_'):
    416             prompt_player_fsd_dialog(e.msg, e.dict)
    417             if e.msg == 'SystemCheck_JumpFailed_StandingsTooLow':
    418                 self.SetOff()
    419                 raise
    420         elif e.msg == 'NotCloseEnoughToJump':
    421             park = sm.GetService('michelle').GetRemotePark()
    422             park.CmdSetSpeedFraction(1.0)
    423             if uicore.layer.shipui.isopen:
    424                 uicore.layer.shipui.SetSpeed(1.0)
    425             park.CmdFollowBall(destID, 0.0)
    426             self.LogWarn("Autopilot: I thought I was close enough to jump, but I wasn't.")
    427         elif e.msg in ('JumpNotEnoughCharge3', 'StructureServiceUnavailable', 'CannotUseJumpGateShipTooHeavy', 'NoLinkToDestination'):
    428             self.SetOff()
    429             raise
    430         sys.exc_clear()
    431         self.LogError('Autopilot: jumping to ' + jumpToLocationName + ' failed. Will try again')
    432         self.ignoreTimerCycles = 5
    433 
    434     def AP_ApproachDestID(self, destinationID, isFinal):
    435         self.CancelSystemNavigation()
    436         park = sm.GetService('michelle').GetRemotePark()
    437         park.CmdSetSpeedFraction(1.0)
    438         if uicore.layer.shipui.isopen:
    439             uicore.layer.shipui.SetSpeed(1.0)
    440         park.CmdFollowBall(destinationID, 0.0)
    441         prompt_player_fsd_dialog('AutoPilotApproaching')
    442         if not (isFinal and self.IsStationOrStructure(destinationID)):
    443             sm.GetService('audio').AudioMessage('msg_AutoPilotApproaching_play')
    444         self.LogInfo('Autopilot: approaching')
    445         self.ignoreTimerCycles = 2
    446 
    447     def AP_WarpToDestID(self, destinationID, name, isFinal):
    448         try:
    449             sm.GetService('space').WarpDestination(celestialID=destinationID)
    450             sm.GetService('michelle').GetRemotePark().CmdWarpToStuffAutopilot(destinationID)
    451             prompt_player_fsd_dialog('AutoPilotWarpingTo', {'what': name})
    452             if isFinal:
    453                 if self.IsStationOrStructure(destinationID):
    454                     sm.GetService('audio').AudioMessage('msg_AutoPilotWarpingToStation_play')
    455                 self.LogInfo('Autopilot: warping to celestial object', destinationID)
    456             else:
    457                 sm.GetService('audio').AudioMessage('msg_AutoPilotWarpingTo_play')
    458                 self.LogInfo('Autopilot: warping to gate')
    459             sm.ScatterEvent('OnAutoPilotWarp')
    460             self.ignoreTimerCycles = 2
    461         except UserError as e:
    462             sys.exc_clear()
    463             item = sm.GetService('godma').GetItem(session.shipid)
    464             if item.warpScrambleStatus > 0:
    465                 self.SetOff('Autopilot cannot warp while warp scrambled.')
    466             if 'WarpDisrupted' in e.msg:
    467                 self.SetOff('Autopilot cannot warp while warp scrambled by bubble.')
    468         except Exception:
    469             self.SetOff('Unknown error')
    470 
    471     def NavigateSystemTo(self, itemID, interactionRange, commandFunc, *args, **kwargs):
    472         self.LogInfo('Navigate to item', itemID, 'range', interactionRange, 'and execute', commandFunc)
    473         self.__navigateSystemDestinationItemID = itemID
    474         self.__navigateSystemThread = AutoTimer(50, self.__NavigateSystemTo, itemID, interactionRange, commandFunc, *args, **kwargs)
    475 
    476     def CancelSystemNavigation(self):
    477         self.LogInfo('Cancel system navigation')
    478         self.__navigateSystemDestinationItemID = None
    479         self.__navigateSystemThread = None
    480 
    481     def IsSystemNavigationComplete(self, itemID, interactionRange):
    482         return not self.InWarp() and self.InInteractionRange(itemID, interactionRange) and not self.IsCloaked()
    483 
    484     def __NavigateSystemTo(self, itemID, interactionRange, commandFunc, *args, **kwargs):
    485         try:
    486             if self.InWarp():
    487                 pass
    488             elif self.InInteractionRange(itemID, interactionRange) and not self.IsCloaked():
    489                 self.LogInfo('System navigation: at target location. Triggering action')
    490                 try:
    491                     commandFunc(*args, **kwargs)
    492                 except UserError:
    493                     raise
    494                 finally:
    495                     self.CancelSystemNavigation()
    496 
    497             elif self.InWarpRange(itemID):
    498                 self.LogInfo('System navigation: warping to target', itemID, interactionRange)
    499                 WarpToItem(itemID, warpRange=const.minWarpEndDistance, cancelAutoNavigation=False)
    500             elif self.IsApproachable(itemID):
    501                 movementFunctions.ShipApproach(itemID, cancelAutoNavigation=False)
    502             else:
    503                 self.LogInfo('Unable to resolve the proper navigation action. Aborting.', itemID, interactionRange, commandFunc)
    504                 self.CancelSystemNavigation()
    505             if self.__navigateSystemThread:
    506                 self.__navigateSystemThread.interval = AUTO_NAVIGATION_LOOP_INTERVAL_MS
    507         except UserError as e:
    508             self.LogInfo('User error detected', e.msg, itemID, interactionRange, commandFunc)
    509             raise
    510         except Exception as e:
    511             if isinstance(e, RuntimeError) and e.args[0] in ('MonikerSessionCheckFailure', 'Ship reattaching while in process of departing', 'Missing shipid', 'User not in any solar system'):
    512                 self.LogInfo('Exception ignored due to session changing while still trying to navigate.', itemID, interactionRange, commandFunc)
    513             else:
    514                 self.LogError('Problem while navigating system', itemID, interactionRange, commandFunc)
    515                 log.LogException(channel=self.__guid__)
    516             self.CancelSystemNavigation()
    517 
    518     def IsApproachable(self, itemID):
    519         destBall = self.michelle.GetBall(itemID)
    520         if destBall is not None and destBall.surfaceDist < const.minWarpDistance:
    521             return True
    522         return False
    523 
    524     def InInteractionRange(self, itemID, interactionRange):
    525         destBall = self.michelle.GetBall(itemID)
    526         if destBall is not None and destBall.surfaceDist < interactionRange:
    527             return True
    528         return False
    529 
    530     def InWarp(self):
    531         shipBall = self.michelle.GetBall(session.shipid)
    532         if shipBall is not None and shipBall.mode == destiny.DSTBALL_WARP:
    533             return True
    534         return False
    535 
    536     def InWarpRange(self, itemID):
    537         destBall = self.michelle.GetBall(itemID)
    538         if destBall is not None and destBall.surfaceDist > const.minWarpDistance:
    539             return True
    540         return False
    541 
    542     def IsCloaked(self):
    543         shipBall = self.michelle.GetBall(session.shipid)
    544         if shipBall is not None:
    545             return bool(shipBall.isCloaked)
    546         return False
    547 
    548     def OnCharacterSessionChanged(self, _oldCharacterID, newCharacterID):
    549         if newCharacterID is not None:
    550             self.updateJumpGatesTimer = AutoTimer(const.HOUR / const.MSEC, self.UpdateJumpGatesTimerThread)
    551 
    552     def OnSessionReset(self):
    553         if self.updateJumpGatesTimer:
    554             self.updateJumpGatesTimer.KillTimer()
    555         self.updateJumpGatesTimer = None
    556 
    557     def UpdateJumpGatesTimerThread(self):
    558         if not settings.char.ui.Get('pathFinder_includeJumpGates', False):
    559             return
    560         with ExceptionEater('UpdateJumpGatesTimerThread'):
    561             self.VerifyJumpGatesInRoute()
    562 
    563     def FindJumpGatesInRoute(self):
    564         starmapSvc = sm.GetService('starmap')
    565         path = starmapSvc.GetRouteFromWaypoints(starmapSvc.GetWaypoints())
    566         if len(path) < 2:
    567             return []
    568         lastKnownJumpGates = self.clientPathfinderService.GetLastKnownJumpGates()
    569         potentialJumpGatePairs = []
    570         for fromLocationID, toLocationID in itertools.izip(path, path[1:]):
    571             if (fromLocationID, toLocationID) in lastKnownJumpGates:
    572                 potentialJumpGatePairs.append((fromLocationID, toLocationID))
    573 
    574         jumpGatesInRoute = []
    575         for fromLocationID, toLocationID in potentialJumpGatePairs:
    576             if not self.GetGateIDToSolarsystemFromLocationID(toLocationID, fromLocationID):
    577                 jGate = lastKnownJumpGates.get((fromLocationID, toLocationID), None)
    578                 if jGate:
    579                     jumpGatesInRoute.append(jGate)
    580 
    581         return jumpGatesInRoute
    582 
    583     def VerifyJumpGatesInRoute(self):
    584         with ExceptionEater('VerifyJumpGatesInRoute'):
    585             if not settings.char.ui.Get('pathFinder_includeJumpGates', False):
    586                 return
    587             jumpGates = self.FindJumpGatesInRoute()
    588             structureServicesSvc = sm.GetService('structureServices')
    589             needsRefresh = False
    590             for jGate in jumpGates:
    591                 availableServices = structureServicesSvc.CharacterGetServices(jGate)
    592                 if SERVICE_JUMP_BRIDGE not in availableServices:
    593                     needsRefresh = True
    594                     break
    595 
    596             if needsRefresh:
    597                 self.UpdateJumpGatesAndRoute()
    598 
    599     def UpdateJumpGatesAndRoute(self):
    600         self.clientPathfinderService.UpdateJumpGates()
    601         sm.GetService('autoPilot').OptimizeRoute()
    602 
    603     def OnMapShowsJumpGatesChanged(self):
    604         self.UpdateJumpGatesAndRoute()
    605 
    606     def OptimizeRoute(self, *args):
    607         if self.isOptimizing:
    608             return
    609         try:
    610             self.isOptimizing = True
    611             starmapSvc = sm.GetService('starmap')
    612             waypoints = list(starmapSvc.GetWaypoints())
    613             originalWaypointsLen = len(waypoints)
    614             isReturnTrip = False
    615             for idx in reversed(xrange(len(waypoints))):
    616                 if waypoints[idx] == session.solarsystemid2:
    617                     del waypoints[idx]
    618                     isReturnTrip = True
    619                     break
    620 
    621             solarSystemToStations = defaultdict(list)
    622             for i, waypoint in enumerate(waypoints):
    623                 if self.IsStationOrStructure(waypoint):
    624                     if idCheckers.IsStation(waypoint):
    625                         solarSystemID = cfg.stations.Get(waypoint).solarSystemID
    626                     else:
    627                         solarSystemID = self.structureDirectory.GetStructureInfo(waypoint).solarSystemID
    628                     solarSystemToStations[solarSystemID].append(waypoint)
    629                     waypoints[i] = solarSystemID
    630 
    631             waypoints = list(set(waypoints))
    632             if session.solarsystemid2 in waypoints:
    633                 waypoints.remove(session.solarsystemid2)
    634             numWaypoints = len(waypoints)
    635             if numWaypoints == 0:
    636                 return
    637             msg = None
    638             if numWaypoints > 12:
    639                 msg = 'UI/Map/MapPallet/msgOptimizeQuestion1'
    640             elif numWaypoints > 10:
    641                 msg = 'UI/Map/MapPallet/msgOptimizeQuestion2'
    642             if msg:
    643                 yesNo = prompt_player_fsd_dialog('AskAreYouSure', {'cons': localization.GetByLabel(msg, numWaypoints=originalWaypointsLen)}, uiconst.YESNO)
    644                 if yesNo != uiconst.ID_YES:
    645                     return
    646             distance = {}
    647             waypoints.append(session.solarsystemid2)
    648             for fromID in waypoints:
    649                 distance[fromID] = {}
    650                 for toID in waypoints:
    651                     if fromID == toID:
    652                         continue
    653                     distance[fromID][toID] = self.clientPathfinderService.GetAutopilotJumpCount(toID, fromID)
    654 
    655             waypoints.pop()
    656             startTime = blue.os.GetWallclockTimeNow()
    657             prefix = [None]
    658             _push = prefix.append
    659             _pop = prefix.pop
    660 
    661             def FindShortestRoute(prefix, distanceSoFar, toID):
    662                 distanceTo = distance[toID]
    663                 prefix[-1] = toID
    664                 shortestDist = shortestRouteSoFar[0]
    665                 if len(prefix) < numWaypoints:
    666                     _push(None)
    667                     for i in indexes:
    668                         toID = waypoints[i]
    669                         if not toID:
    670                             continue
    671                         candidateDist = distanceSoFar + distanceTo[toID]
    672                         if candidateDist >= shortestDist:
    673                             continue
    674                         waypoints[i] = None
    675                         FindShortestRoute(prefix, candidateDist, toID)
    676                         waypoints[i] = toID
    677 
    678                     _pop()
    679                 else:
    680                     for i in indexes:
    681                         toID = waypoints[i]
    682                         if not toID:
    683                             continue
    684                         candidateDist = distanceSoFar + distanceTo[toID]
    685                         if candidateDist < shortestDist:
    686                             shortestRouteSoFar[:] = [candidateDist, prefix[:], toID]
    687                             shortestDist = candidateDist
    688 
    689             shortestRouteSoFar = [999999999, None, None]
    690             indexes = range(len(waypoints))
    691             FindShortestRoute(prefix, 0, session.solarsystemid2)
    692             distance, waypoints, last = shortestRouteSoFar
    693             if waypoints is not None and waypoints[0] == session.solarsystemid2:
    694                 waypoints = waypoints[1:]
    695             blue.pyos.synchro.SleepWallclock(1)
    696             if waypoints is None:
    697                 raise UserError('AutoPilotDisabledUnreachable')
    698             waypoints.append(last)
    699             waypointsWithStations = []
    700             for waypoint in waypoints:
    701                 if waypoint in solarSystemToStations:
    702                     waypointsWithStations.extend(solarSystemToStations[waypoint])
    703                 else:
    704                     waypointsWithStations.append(waypoint)
    705 
    706             if isReturnTrip == True:
    707                 sm.GetService('starmap').SetWaypoints(waypointsWithStations + [session.solarsystemid2])
    708             else:
    709                 sm.GetService('starmap').SetWaypoints(waypointsWithStations)
    710         finally:
    711             self.isOptimizing = False
    712 
    713     def _GetNextItemIDInRoute(self):
    714         destinationPath = sm.GetService('starmap').GetDestinationPath()
    715         if destinationPath:
    716             return destinationPath[0]
    717 
    718     def GetNextItemIDInRoute(self):
    719         itemID = self._GetNextItemIDInRoute()
    720         if not itemID:
    721             return
    722         bp = sm.GetService('michelle').GetBallpark()
    723         if not bp:
    724             return
    725         for slimItem in bp.slimItems.values():
    726             if slimItem.jumps and slimItem.jumps[0].locationID == itemID:
    727                 return slimItem.itemID
    728             if slimItem.itemID == itemID:
    729                 return itemID