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

fxSequencer.py (27288B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\eve\client\script\parklife\fxSequencer.py
      2 import sys
      3 import blue
      4 import blue.heapq as heapq
      5 import log
      6 import telemetry
      7 import evegraphics.settings as gfxsettings
      8 import locks
      9 import uthread
     10 import utillib
     11 from carbon.common.script.sys.service import Service
     12 from carbon.common.script.util.commonutils import IsFullLogging
     13 from eve.client.script.environment.effects.GenericEffect import GenericEffect, STOP_REASON_BALL_REMOVED, STOP_REASON_DEFAULT
     14 from eve.client.script.environment.effects.Repository import GetClassification
     15 from eve.client.script.environment.effects.effectConsts import FX_MERGE_GUID, FX_MERGE_MODULE, FX_MERGE_SHIP, FX_MERGE_TARGET
     16 from eve.client.script.environment.effects.shipRenderEffect import ShipRenderEffect
     17 from eve.client.script.environment.spaceObject.playerOwnedStructure import PlayerOwnedStructure
     18 from eveexceptions import ExceptionEater
     19 from evegraphics.explosions.spaceObjectExplosionManager import SpaceObjectExplosionManager
     20 from fsdBuiltData.client.effects import GetEffectGuids
     21 from destructionEffect import destructionType as destructionEffectType
     22 from fsdBuiltData.client.travelEffects import IsTravelEffect
     23 SECOND = 10000000
     24 FX_TURRET_EFFECT_GUIDS = ['effects.Laser',
     25  'effects.ProjectileFiredForEntities',
     26  'effects.ProjectileFired',
     27  'effects.HybridFired',
     28  'effects.TractorBeam',
     29  'effects.Salvaging',
     30  'effects.TriglavianBeam']
     31 FX_PROTECTED_EFFECT_GUIDS = ['effects.GateActivity',
     32  'effects.WormholeActivity',
     33  'effects.JumpIn',
     34  'effects.JumpOut',
     35  'effects.JumpOutWormhole',
     36  'effects.JumpOutAbyssal',
     37  'effects.JumpOutAbyssalBetween',
     38  'effects.JumpBridgeIn',
     39  'effects.JumpBridgeOut',
     40  'effects.Warping',
     41  'effects.Cloaking',
     42  'effects.Uncloak',
     43  'effects.Cloak',
     44  'effects.CloakNoAmim',
     45  'effects.CloakRegardless',
     46  'effects.StructureOffline',
     47  'effects.StructureOnlined',
     48  'effects.AnchorDrop',
     49  'effects.AnchorLift',
     50  'effects.SiegeMode',
     51  'effects.TriageMode',
     52  'effects.WarpDisruptFieldGenerating',
     53  'effects.WarpScramble',
     54  'effects.Tethering',
     55  'effects.StellarHarvester',
     56  'effects.DynamicDisruptionBubble']
     57 FX_LONG_ONESHOT_GUIDS = ['effects.SuperWeaponAmarr',
     58  'effects.SuperWeaponCaldari',
     59  'effects.SuperWeaponGallente',
     60  'effects.SuperWeaponMinmatar']
     61 
     62 class Trigger(utillib.KeyVal):
     63 
     64     def __le__(self, other):
     65         return self.stamp <= other.stamp
     66 
     67     def __eq__(self, other):
     68         return self.shipID == other.shipID and self.moduleID == other.moduleID and self.targetID == other.targetID and self.guid == other.guid
     69 
     70 
     71 class Activation():
     72 
     73     def __init__(self, broker):
     74         self.triggers = []
     75         self.effect = None
     76         self.guid = None
     77         self.stamp = -1
     78         self.duration = -1
     79         self.broker = broker
     80         self.key = None
     81         self.isOn = False
     82         self.balls = set()
     83         self.isZombie = False
     84 
     85     def __le__(self, other):
     86         return self.stamp <= other.stamp
     87 
     88     def Zombify(self):
     89         self.isZombie = True
     90         self.triggers = []
     91         self.balls = set()
     92         self.broker = None
     93 
     94     def TriggerAlreadyExists(self, trigger):
     95         for t in self.triggers:
     96             if trigger.moduleID == t.moduleID:
     97                 return True
     98 
     99         return False
    100 
    101     @telemetry.ZONE_METHOD
    102     def AddTrigger(self, trigger):
    103         if self.TriggerAlreadyExists(trigger):
    104             return
    105         heapq.heappush(self.triggers, trigger)
    106         stampUpdated = self.UpdateRepeatTime()
    107         if self.effect is None:
    108             try:
    109                 self.effect = self.GetEffectFromGuid(trigger)
    110             except:
    111                 self.effect = GenericEffect(trigger)
    112                 log.LogException()
    113                 sys.exc_clear()
    114 
    115             self.guid = trigger.guid
    116             self.balls = set(self.effect.GetBalls())
    117             self.PrepareAndStart()
    118             self.isOn = True
    119         else:
    120             self.balls.add(trigger.shipID)
    121             self.balls.add(trigger.targetID)
    122         return stampUpdated
    123 
    124     @telemetry.ZONE_METHOD
    125     def PrepareAndStart(self):
    126         try:
    127             self.effect.Prepare()
    128             self.effect.Start(self.duration)
    129         except:
    130             log.LogException()
    131             sys.exc_clear()
    132 
    133     @telemetry.ZONE_METHOD
    134     def RemoveTrigger(self, trigger):
    135         try:
    136             ix = self.triggers.index(trigger)
    137         except ValueError:
    138             ix = -1
    139 
    140         if ix >= 0:
    141             del self.triggers[ix]
    142             heapq.heapify(self.triggers)
    143             self.PurgeTriggers()
    144             return self.UpdateRepeatTime()
    145 
    146     @telemetry.ZONE_METHOD
    147     def UpdateRepeatTime(self):
    148         lastTime = -1
    149         candidate = None
    150         for trigger in self.triggers:
    151             if trigger.stamp > lastTime:
    152                 lastTime = trigger.stamp
    153                 candidate = trigger
    154 
    155         if candidate is None:
    156             return -1
    157         now = blue.os.GetSimTime()
    158         duration = int(candidate.duration)
    159         durationInBluetime = SECOND * duration / 1000
    160         wholeRepeatsLeft = long((candidate.stamp - now) / durationInBluetime)
    161         if wholeRepeatsLeft == candidate.repeat:
    162             wholeRepeatsLeft -= 1
    163         newStamp = candidate.stamp - wholeRepeatsLeft * durationInBluetime
    164         if newStamp == self.stamp:
    165             return 0
    166         self.stamp = newStamp
    167         self.duration = candidate.duration
    168         return 1
    169 
    170     @telemetry.ZONE_METHOD
    171     def Repeat(self):
    172         if self.effect is None:
    173             raise RuntimeError('Activation::Repeat: No effect defined')
    174         stampUpdated = self.PurgeTriggers()
    175         if not self.triggers:
    176             self.Stop()
    177             return -1
    178         if not self.isOn:
    179             return stampUpdated
    180         try:
    181             self.effect.Repeat(self.duration)
    182         except:
    183             log.LogException()
    184             sys.exc_clear()
    185 
    186         return stampUpdated
    187 
    188     @telemetry.ZONE_METHOD
    189     def Stop(self, reason = STOP_REASON_DEFAULT):
    190         if not self.isOn:
    191             return
    192         if self.effect is None:
    193             raise RuntimeError('Activation::Stop: No effect defined')
    194         try:
    195             self.effect.Stop(reason)
    196         except:
    197             log.LogException()
    198             sys.exc_clear()
    199 
    200         self.isOn = False
    201 
    202     @telemetry.ZONE_METHOD
    203     def PurgeTriggers(self):
    204         now = blue.os.GetSimTime()
    205         while self.triggers and self.triggers[0].stamp <= now:
    206             heapq.heappop(self.triggers)
    207 
    208         return self.UpdateRepeatTime()
    209 
    210     def GetBalls(self):
    211         return self.balls
    212 
    213     def GetName(self):
    214         if self.triggers:
    215             return self.triggers[0].guid
    216         else:
    217             return 'unknown'
    218 
    219     def GetEffectFromGuid(self, trigger):
    220         classification = GetClassification(trigger.guid)
    221         if classification is None:
    222             raise RuntimeError('Activation::Unable to load effect for guid:', trigger.guid)
    223         classType = classification[0]
    224         args = classification[1:]
    225         return classType(trigger, *args)
    226 
    227 
    228 def GetFxSequencer():
    229     return sm.GetService('FxSequencer')
    230 
    231 
    232 class FxSequencer(Service):
    233     __guid__ = 'svc.FxSequencer'
    234     __exportedcalls__ = {'ClearAll': [],
    235      'NotifyModelLoaded': [],
    236      'EnableGuids': [],
    237      'DisableGuids': [],
    238      'GetDisabledGuids': []}
    239     __notifyevents__ = ['OnSpecialFX',
    240      'DoBallRemove',
    241      'DoSimClockRebase',
    242      'DoBallsRemove',
    243      'OnGraphicSettingsChanged']
    244     __dependencies__ = ['michelle']
    245     __startupdependencies__ = ['settings']
    246 
    247     def __init__(self):
    248         Service.__init__(self)
    249         self.activations = {}
    250         self.stopCandidates = []
    251         self.ballRegister = {}
    252         self.delayedtriggers = {}
    253         self.killLoop = True
    254         self.sequencerTasklet = None
    255         self.disabledGuids = {}
    256 
    257     def Run(self, memStream = None):
    258         Service.Run(self, memStream)
    259         self.lock = locks.RLock()
    260         self.disabledGuids = settings.user.ui.Get('disabledGuids', {})
    261         self.sequencerTasklet = uthread.new(self.SequencerLoop)
    262         self.sequencerTasklet.context = 'FxSequencer::SequencerLoop'
    263         if gfxsettings.Get(gfxsettings.UI_TURRETS_ENABLED):
    264             self.EnableGuids(FX_TURRET_EFFECT_GUIDS)
    265         else:
    266             self.DisableGuids(FX_TURRET_EFFECT_GUIDS)
    267         candidateEffects = []
    268         for guid in GetEffectGuids():
    269             if guid not in FX_TURRET_EFFECT_GUIDS and guid not in FX_PROTECTED_EFFECT_GUIDS:
    270                 candidateEffects.append(guid)
    271 
    272         if gfxsettings.Get(gfxsettings.UI_EFFECTS_ENABLED):
    273             self.EnableGuids(candidateEffects)
    274         else:
    275             self.DisableGuids(candidateEffects)
    276 
    277     def Stop(self, ms):
    278         Service.Stop(self)
    279         self.killLoop = True
    280         blue.pyos.synchro.WakeupAtSim(self.sequencerTasklet, 0, -1)
    281         self.sequencerTasklet = None
    282 
    283     def DoSimClockRebase(self, times):
    284         oldSimTime, newSimTime = times
    285         offset = newSimTime - oldSimTime
    286         for activation in self.activations.itervalues():
    287             activation.stamp += offset
    288             for trigger in activation.triggers:
    289                 with ExceptionEater('Adjusting time on an FX trigger'):
    290                     trigger.stamp += offset
    291                     if trigger.startTime:
    292                         trigger.startTime += offset
    293 
    294         for triggerList in self.delayedtriggers.itervalues():
    295             for trigger in triggerList:
    296                 with ExceptionEater('Adjusting time on a delayed FX trigger'):
    297                     if trigger.startTime:
    298                         trigger.startTime += offset
    299 
    300     def OnGraphicSettingsChanged(self, changes):
    301         if gfxsettings.UI_TURRETS_ENABLED in changes:
    302             if gfxsettings.Get(gfxsettings.UI_TURRETS_ENABLED):
    303                 self.EnableGuids(fxTurretGuids)
    304             else:
    305                 self.DisableGuids(fxTurretGuids)
    306         if gfxsettings.UI_EFFECTS_ENABLED in changes:
    307             candidateEffects = []
    308             for guid in GetEffectGuids():
    309                 if guid not in fxTurretGuids and guid not in fxProtectedGuids:
    310                     candidateEffects.append(guid)
    311 
    312             if len(candidateEffects) > 0:
    313                 if gfxsettings.Get(gfxsettings.UI_EFFECTS_ENABLED):
    314                     self.EnableGuids(candidateEffects)
    315                 else:
    316                     self.DisableGuids(candidateEffects)
    317 
    318     @telemetry.ZONE_METHOD
    319     def OnSpecialFX(self, shipID, moduleID, moduleTypeID, targetID, otherTypeID, guid, isOffensive, start, active, duration = -1, repeat = None, startTime = None, timeFromStart = 0, graphicInfo = None):
    320         if start == 1 and guid in self.disabledGuids:
    321             return
    322         if IsTravelEffect(guid) and sm.GetService('subway').Enabled():
    323             return
    324         if startTime is not None and guid in FX_LONG_ONESHOT_GUIDS:
    325             now = blue.os.GetSimTime()
    326             if now - startTime > 150000000:
    327                 return
    328         classification = GetClassification(guid)
    329         if classification is not None:
    330             effect = classification[1]
    331             mergeFlags = getattr(effect, 'mergeFlags')
    332         else:
    333             effect = None
    334             mergeFlags = [0]
    335         trigger = Trigger(shipID=shipID, moduleID=moduleID, moduleTypeID=moduleTypeID, targetID=targetID, otherTypeID=otherTypeID, guid=guid, isOffensive=isOffensive, duration=duration, repeat=repeat, startTime=startTime, timeFromStart=timeFromStart, graphicInfo=graphicInfo, mergeFlags=mergeFlags, controllerVariableTriggers=getattr(effect, 'controllerVariableTriggers', {}), locatorSetOverride=getattr(effect, 'locatorSetOverride', None))
    336         if trigger.repeat is None or trigger.repeat <= 0:
    337             trigger.repeat = 1
    338         else:
    339             trigger.repeat += 1
    340         if guid is None:
    341             self.LogWarn('FxSequencer::OnSpecialFx: No guid in trigger for moduleTypeID', moduleTypeID, 'and graphicInfo', graphicInfo)
    342             return
    343         splitGuid = guid.split('.')
    344         if len(splitGuid) != 2:
    345             self.LogInfo('FxSequencer::OnSpecialFx: No guid in trigger')
    346             return
    347         niceName = guid.split('.')[1]
    348         if shipID == session.shipid:
    349             ship = 'for myself'
    350         elif IsFullLogging():
    351             ship = 'for ship: %s' % shipID
    352         else:
    353             ship = 'for another ship'
    354         if start == 1:
    355             if active == 0:
    356                 self.LogInfo('FxSequencer::OnSpecialFX: Got ONE-SHOT event:', niceName, ship, '[', moduleID, '] with duration:', duration)
    357             if trigger.repeat is not None and trigger.repeat > 0:
    358                 self.LogInfo('Starting REPEAT event:', niceName, ship, '[', moduleID, '] with duration:', duration / 1000.0, 'and repeat:', trigger.repeat)
    359                 self.AddTrigger(trigger)
    360             else:
    361                 self.LogInfo('FxSequencer::OnSpecialFX: Starting TOGGLE event:', niceName, ship, '[', moduleID, ']')
    362             if targetID is not None:
    363                 if targetID == session.shipid:
    364                     target = 'myself'
    365                 else:
    366                     target = targetID
    367                 self.LogInfo('targeted at:', target, 'and is', ['not', ''][isOffensive], 'offensive')
    368         else:
    369             self.RemoveTrigger(trigger)
    370             self.LogInfo('FxSequencer::OnSpecialFX: Stopping', niceName, 'for ship:', ship, 'and module:', moduleID)
    371 
    372     def EnableGuids(self, guids):
    373         for guid in guids:
    374             if guid in self.disabledGuids:
    375                 del self.disabledGuids[guid]
    376 
    377         settings.user.ui.Set('disabledGuids', self.disabledGuids)
    378         self.LogInfo('FxSequencer::EnableGuids', guids, ' Current disabled guids:', self.disabledGuids)
    379 
    380     def DisableGuids(self, guids):
    381         for guid in guids:
    382             if guid not in self.disabledGuids and guid not in FX_PROTECTED_EFFECT_GUIDS:
    383                 self.disabledGuids[guid] = None
    384 
    385         settings.user.ui.Set('disabledGuids', self.disabledGuids)
    386         self.LogInfo('FxSequencer::DisableGuids', guids, ' Current disabled guids:', self.disabledGuids)
    387 
    388     def GetDisabledGuids(self):
    389         return self.disabledGuids
    390 
    391     @telemetry.ZONE_METHOD
    392     def AddTrigger(self, trigger):
    393         with self.lock:
    394             telemetry.APPEND_TO_ZONE(trigger.guid)
    395             self.LogInfo('FxSequencer::AddTrigger: Entering')
    396             shipBall = None
    397             targetBall = None
    398             if trigger.shipID is not None:
    399                 shipBall = self.GetBall(trigger.shipID)
    400             if trigger.targetID is not None:
    401                 targetBall = self.GetBall(trigger.targetID)
    402             classification = GetClassification(trigger.guid)
    403             if trigger.shipID is not None and shipBall is None:
    404                 self.LogWarn('FxSequencer::AddTrigger: ship not in ballpark. Trigger ignored')
    405                 return
    406             if classification and classification[0] is ShipRenderEffect:
    407                 if hasattr(shipBall, 'model') and not hasattr(shipBall.model, 'overlayEffects'):
    408                     self.LogInfo('This ball', shipBall.id, 'of type', self.GetItem(trigger.shipID).typeID, 'does not support the ship effect', trigger.guid)
    409                     return
    410             if isinstance(shipBall, PlayerOwnedStructure) and getattr(trigger, 'moduleID', None):
    411                 trigger.shipID = trigger.moduleID
    412             if trigger.targetID is not None and targetBall is None:
    413                 self.LogWarn('FxSequencer::AddTrigger: target not in ballpark. Trigger ignored')
    414                 return
    415             delayed = False
    416             if trigger.shipID is not None and getattr(shipBall, 'model', None) is None:
    417                 if trigger.shipID in self.delayedtriggers:
    418                     self.delayedtriggers[trigger.shipID].append(trigger)
    419                 else:
    420                     self.delayedtriggers[trigger.shipID] = [trigger]
    421                 delayed = True
    422             if trigger.targetID is not None and getattr(targetBall, 'model', None) is None:
    423                 if trigger.targetID in self.delayedtriggers:
    424                     self.delayedtriggers[trigger.targetID].append(trigger)
    425                 else:
    426                     self.delayedtriggers[trigger.targetID] = [trigger]
    427                 delayed = True
    428             if delayed:
    429                 return
    430             key = self.GetKey(trigger)
    431             if trigger.duration is None or trigger.duration <= 0.0:
    432                 if classification is not None:
    433                     trigger.duration = getattr(classification[1], 'duration', None)
    434             if trigger.repeat is None or trigger.repeat == 0:
    435                 trigger.repeat = 1
    436             trigger.__dict__['stamp'] = blue.os.GetSimTime() + long(trigger.duration * trigger.repeat / 1000.0 * SECOND)
    437             if key in self.activations and trigger.guid not in ('effects.Cloak', 'effects.Uncloak'):
    438                 activation = self.activations[key]
    439                 activation.AddTrigger(trigger)
    440                 self.AddActivationToBallRegister(activation)
    441                 effect = activation.effect
    442                 if effect and trigger.graphicInfo:
    443                     effect.UpdateGraphicInfo(trigger.graphicInfo)
    444             else:
    445                 self.AddActivation(trigger, key)
    446         self.LogInfo('FxSequencer::AddTrigger: Done')
    447 
    448     @telemetry.ZONE_METHOD
    449     def RemoveTrigger(self, trigger):
    450         self.LogInfo('FxSequencer::RemoveTrigger: Entering')
    451         shipBall = None
    452         if trigger.shipID is not None:
    453             shipBall = self.GetBall(trigger.shipID)
    454         if getattr(shipBall, '__guid__', '') == 'spaceObject.PlayerOwnedStructure' and getattr(trigger, 'moduleID', None) is not None:
    455             trigger.shipID = trigger.moduleID
    456         key = self.GetKey(trigger)
    457         if key not in self.activations:
    458             self.LogInfo('FxSequencer::RemoveTrigger: Trigger not found')
    459             return
    460         activation = self.activations[key]
    461         updateTime = activation.RemoveTrigger(trigger)
    462         if updateTime == -1:
    463             self.LogInfo('FxSequencer::RemoveTrigger: Activation empty. Deleting it.')
    464             self.RemoveActivation(activation, fromSequencer=True)
    465         self.LogInfo('FxSequencer::RemoveTrigger:', trigger.guid, 'Done')
    466 
    467     @telemetry.ZONE_METHOD
    468     def SequencerLoop(self):
    469         self.killLoop = False
    470         reason = None
    471         self.LogInfo('FxSequencer: Sequencer loop starting')
    472         while not getattr(self, 'killLoop', True):
    473             self.CheckForStopCandidates()
    474             if self.stopCandidates:
    475                 wakeupTime = self.stopCandidates[0].stamp
    476             else:
    477                 wakeupTime = blue.os.GetSimTime() + 60 * SECOND
    478             reason = blue.pyos.synchro.SleepUntilSim(wakeupTime)
    479 
    480         self.LogInfo('FxSequencer: Sequencer loop terminated with reason', reason)
    481 
    482     @telemetry.ZONE_METHOD
    483     def CheckForStopCandidates(self):
    484         now = blue.os.GetSimTime()
    485         while self.stopCandidates and self.stopCandidates[0].isZombie:
    486             heapq.heappop(self.stopCandidates)
    487 
    488         while self.stopCandidates and self.stopCandidates[0].stamp < now:
    489             activation = heapq.heappop(self.stopCandidates)
    490             state = activation.Repeat()
    491             if state == -1:
    492                 self.LogInfo('FxSequencer::CheckForStopCandidates: activation expired')
    493                 self.RemoveActivation(activation)
    494             else:
    495                 heapq.heappush(self.stopCandidates, activation)
    496             while self.stopCandidates and self.stopCandidates[0].isZombie:
    497                 heapq.heappop(self.stopCandidates)
    498 
    499             now = blue.os.GetSimTime()
    500 
    501     @telemetry.ZONE_METHOD
    502     def DoBallsRemove(self, pythonBalls, isRelease):
    503         for ball, slimItem, terminal in pythonBalls:
    504             self.RemoveAllBallActivations(ball.id)
    505 
    506     @telemetry.ZONE_METHOD
    507     def DoBallRemove(self, ball, slimItem, terminal):
    508         self.RemoveAllBallActivations(ball.id)
    509 
    510     @telemetry.ZONE_METHOD
    511     def GetKey(self, trigger):
    512         key = []
    513         if FX_MERGE_GUID in trigger.mergeFlags:
    514             key.append(trigger.guid)
    515         if FX_MERGE_SHIP in trigger.mergeFlags:
    516             key.append(trigger.shipID)
    517         if FX_MERGE_MODULE in trigger.mergeFlags:
    518             key.append(trigger.moduleID)
    519         if FX_MERGE_TARGET in trigger.mergeFlags:
    520             key.append(trigger.targetID)
    521         if len(key) == 0:
    522             log.LogWarn('Key is empty for trigger:', trigger)
    523             return
    524         return tuple(key)
    525 
    526     @telemetry.ZONE_METHOD
    527     def AddActivation(self, trigger, key = None):
    528         if key is None:
    529             key = self.GetKey(trigger)
    530         activation = Activation(self)
    531         activation.key = key
    532         self.activations[key] = activation
    533         activation.AddTrigger(trigger)
    534         self.AddActivationToBallRegister(activation)
    535         self.AddActivationToSequencer(activation)
    536 
    537     @telemetry.ZONE_METHOD
    538     def AddActivationToSequencer(self, activation):
    539         currentStopTime = activation.stamp
    540         reschedule = True
    541         if self.stopCandidates:
    542             if self.stopCandidates[0].stamp <= currentStopTime:
    543                 reschedule = False
    544         heapq.heappush(self.stopCandidates, activation)
    545         if reschedule:
    546             try:
    547                 self.LogInfo('AddActivationToSequencer: just going for some rescheduling here')
    548                 blue.pyos.synchro.WakeupAtSim(self.sequencerTasklet, currentStopTime)
    549             except:
    550                 self.LogInfo('AddActivationToSequencer: Activation rescedule failed, sequencer will wake up by itself')
    551                 sys.exc_clear()
    552 
    553         self.LogInfo('AddActivationToSequencer: Activation added')
    554 
    555     @telemetry.ZONE_METHOD
    556     def RemoveActivation(self, activation, fromSequencer = False, reason = STOP_REASON_DEFAULT):
    557         niceName = activation.GetName()
    558         activation.Stop(reason)
    559         if activation.key in self.activations:
    560             del self.activations[activation.key]
    561         self.RemoveActivationFromBallRegister(activation)
    562         if fromSequencer:
    563             self.RemoveActivationFromSequencer(activation)
    564         self.LogInfo('FxSequencer::RemoveActivation:', niceName, 'activation removed.', len(self.activations), 'remaining')
    565 
    566     @telemetry.ZONE_METHOD
    567     def RemoveActivationFromSequencer(self, activation):
    568         activation.Zombify()
    569         try:
    570             if self.stopCandidates:
    571                 blue.pyos.synchro.WakeupAtSim(self.sequencerTasklet, self.stopCandidates[0].stamp)
    572             else:
    573                 blue.pyos.synchro.WakeupAtSim(self.sequencerTasklet, blue.os.GetSimTime() + 60 * SECOND)
    574         except:
    575             pass
    576 
    577         self.LogInfo('RemoveActivationFromSequencer:', activation)
    578 
    579     def ClearAll(self):
    580         self.LogInfo('FxSequencer::ClearAll')
    581         for activation in self.activations.values():
    582             self.RemoveActivation(activation, fromSequencer=True)
    583 
    584         self.stopCandidates = []
    585         if len(self.delayedtriggers):
    586             self.LogWarn('FxSequencer::ClearAll: Model Loaded notifications never came through?')
    587             self.delayedtriggers = {}
    588         if len(self.stopCandidates) or len(self.activations) or len(self.ballRegister):
    589             self.LogWarn('FxSequencer::ClearAll: Incomplete reset')
    590 
    591     def AreModelsLoadedForTrigger(self, trigger):
    592         if trigger.targetID is not None and trigger.targetID in self.delayedtriggers:
    593             return False
    594         if trigger.shipID is not None and trigger.shipID in self.delayedtriggers:
    595             return False
    596         return True
    597 
    598     @telemetry.ZONE_METHOD
    599     def NotifyModelLoaded(self, ballID):
    600         if ballID in self.delayedtriggers:
    601             initiateTriggers = self.delayedtriggers.pop(ballID)
    602             self.LogInfo('FxSequencer::NotifyModelLoaded', ballID, 'initiating', len(initiateTriggers), 'triggers')
    603             for trigger in initiateTriggers:
    604                 if self.AreModelsLoadedForTrigger(trigger):
    605                     self.AddTrigger(trigger)
    606 
    607     def GetAllBallActivations(self, ballID):
    608         if ballID in self.ballRegister:
    609             return self.ballRegister[ballID]
    610         return set()
    611 
    612     def GetAllBallActivationNames(self, ballID):
    613         return set([ a.GetName() for a in self.GetAllBallActivations(ballID) ])
    614 
    615     def RemoveAllBallActivations(self, ballID):
    616         if ballID in self.ballRegister:
    617             self.LogInfo('FxSequencer::RemoveAllBallActivations for ball', ballID)
    618             self.RemoveActivationsForBallID(ballID)
    619 
    620     def RemoveActivationAtTime(self, activation, g):
    621         uthread.new(self._RemoveActivationAtTime, activation, g)
    622 
    623     def _RemoveActivationAtTime(self, activation, g):
    624         blue.synchro.SleepUntilSim(g)
    625         self.RemoveActivation(activation, fromSequencer=True, reason=STOP_REASON_BALL_REMOVED)
    626 
    627     def RemoveActivationsForBallID(self, ballID):
    628 
    629         def explosionTimeCallback(sequencer, activationObject):
    630             return lambda g, w: sequencer.RemoveActivationAtTime(activationObject, g)
    631 
    632         for activation in tuple(self.ballRegister.get(ballID, [])):
    633             removeImmediately = True
    634             if ballID == activation.effect.GetEffectTargetID():
    635                 ball = self.GetBall(ballID)
    636                 if destructionEffectType.isExplosionOrOverride(ball.destructionEffectId):
    637                     removeImmediately = False
    638                     SpaceObjectExplosionManager.GetExplosionTimesWhenAvailable(ballID, explosionTimeCallback(self, activation))
    639             if removeImmediately:
    640                 self.RemoveActivation(activation, fromSequencer=True, reason=STOP_REASON_BALL_REMOVED)
    641 
    642     def AddActivationToBallRegister(self, activation):
    643         for ballID in activation.GetBalls():
    644             if ballID is not None:
    645                 if ballID in self.ballRegister:
    646                     self.ballRegister[ballID].add(activation)
    647                 else:
    648                     self.ballRegister[ballID] = set([activation])
    649                 sm.ScatterEvent('OnAddFX', activation.guid, ballID)
    650 
    651     def RemoveActivationFromBallRegister(self, activation):
    652         for ballID in activation.GetBalls():
    653             if ballID in self.ballRegister:
    654                 try:
    655                     self.ballRegister[ballID].remove(activation)
    656                 except KeyError:
    657                     pass
    658 
    659                 if len(self.ballRegister[ballID]) == 0:
    660                     del self.ballRegister[ballID]
    661                 sm.ScatterEvent('OnRemoveFX', activation.guid, ballID)
    662 
    663     def GetBall(self, ballID):
    664         return self.michelle.GetBall(ballID)
    665 
    666     def GetBallpark(self):
    667         return self.michelle.GetBallpark()
    668 
    669     def GetItem(self, itemID):
    670         return self.michelle.GetItem(itemID)
    671 
    672     def GetScene(self):
    673         return sm.GetService('sceneManager').GetRegisteredScene('default')
    674 
    675     def GetTypeAttribute(self, moduleTypeID, attributeEmpFieldRange):
    676         return sm.GetService('godma').GetTypeAttribute(moduleTypeID, attributeEmpFieldRange)
    677 
    678 
    679 fxTurretGuids = FX_TURRET_EFFECT_GUIDS
    680 fxProtectedGuids = FX_PROTECTED_EFFECT_GUIDS
    681 exports = {'FxSequencer.fxTurretGuids': fxTurretGuids,
    682  'FxSequencer.fxProtectedGuids': fxProtectedGuids}