@ -1,7 +1,5 @@
 
		
	
		
			
				 using  BepInEx ;  
		
	
		
			
				using  BepInEx.Configuration ;  
		
	
		
			
				using  CSync.Extensions ;  
		
	
		
			
				using  CSync.Lib ;  
		
	
		
			
				using  HarmonyLib ;  
		
	
		
			
				using  LethalConfig ;  
		
	
		
			
				using  LethalConfig.ConfigItems ;  
		
	
	
		
			
				
					
						
					 
				
			
			@ -21,10 +19,17 @@ using Unity.Netcode;
 
		
	
		
			
				using  UnityEngine ;  
		
	
		
			
				using  UnityEngine.Networking ;  
		
	
		
			
				
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				using  CSync.Extensions ;  
		
	
		
			
				using  CSync.Lib ;  
		
	
		
			
				#endif  
		
	
		
			
				
 
		
	
		
			
				namespace  MuzikaGromche  
		
	
		
			
				{  
		
	
		
			
				    [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)]  
		
	
		
			
				#if  DEBUG  
		
	
		
			
				    [BepInDependency("com.sigurd.csync", "5.0.1")]  
		
	
		
			
				#endif  
		
	
		
			
				    [BepInDependency("ainavt.lc.lethalconfig", "1.4.6")]  
		
	
		
			
				    [BepInDependency("watergun.v72lightfix", BepInDependency.DependencyFlags.SoftDependency)]  
		
	
		
			
				    [BepInDependency("BMX.LobbyCompatibility", BepInDependency.DependencyFlags.HardDependency)]  
		
	
	
		
			
				
					
						
					 
				
			
			@ -470,6 +475,27 @@ namespace MuzikaGromche
 
		
	
		
			
				                FlickerLightsTimeSeries  =  [ - 120.5f ,  - 105 ,  - 89 ,  - 8 ,  44 ,  45 ] , 
 
		
	
		
			
				                Lyrics  =  [ ] , 
 
		
	
		
			
				            } , 
 
		
	
		
			
				            new  Track 
 
		
	
		
			
				            { 
 
		
	
		
			
				                Name  =  "BeefLiver" , 
 
		
	
		
			
				                AudioType  =  AudioType . OGGVORBIS , 
 
		
	
		
			
				                Language  =  Language . ENGLISH , 
 
		
	
		
			
				                WindUpTimer  =  39.35f , 
 
		
	
		
			
				                Bars  =  12 , 
 
		
	
		
			
				                BeatsOffset  =  0.2f , 
 
		
	
		
			
				                ColorTransitionIn  =  0.4f , 
 
		
	
		
			
				                ColorTransitionOut  =  0.4f , 
 
		
	
		
			
				                ColorTransitionEasing  =  Easing . OutExpo , 
 
		
	
		
			
				                Palette  =  Palette . Parse ( [ 
 
		
	
		
			
				                    "#FFEBEB" ,  "#FFEBEB" ,  "#445782" ,  "#EBA602" , 
 
		
	
		
			
				                    "#5EEBB9" ,  "#8EE3DC" ,  "#A23045" ,  "#262222" , 
 
		
	
		
			
				                ] ) , 
 
		
	
		
			
				                LoopOffset  =  0 , 
 
		
	
		
			
				                FadeOutBeat  =  - 3 , 
 
		
	
		
			
				                FadeOutDuration  =  3 , 
 
		
	
		
			
				                FlickerLightsTimeSeries  =  [ - 48 ,  - 40 ,  - 4.5f ,  44 ] , 
 
		
	
		
			
				                Lyrics  =  [ ] , 
 
		
	
		
			
				            } , 
 
		
	
		
			
				        ] ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  static  Track  ChooseTrack ( ) 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -529,6 +555,9 @@ namespace MuzikaGromche
 
		
	
		
			
				
 
		
	
		
			
				        void  Awake ( ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            // Sort in place by name 
 
		
	
		
			
				            Array . Sort ( Tracks . Select ( track  = >  track . Name ) . ToArray ( ) ,  Tracks ) ; 
 
		
	
		
			
				
 
		
	
		
			
				            string  dir  =  Path . GetDirectoryName ( Assembly . GetExecutingAssembly ( ) . Location ) ; 
 
		
	
		
			
				            UnityWebRequest [ ]  requests  =  new  UnityWebRequest [ Tracks . Length  *  2 ] ; 
 
		
	
		
			
				            for  ( int  i  =  0 ;  i  <  Tracks . Length ;  i + + ) 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -549,6 +578,9 @@ namespace MuzikaGromche
 
		
	
		
			
				                    Track  track  =  Tracks [ i ] ; 
 
		
	
		
			
				                    track . LoadedStart  =  DownloadHandlerAudioClip . GetContent ( requests [ i  *  2 ] ) ; 
 
		
	
		
			
				                    track . LoadedLoop  =  DownloadHandlerAudioClip . GetContent ( requests [ i  *  2  +  1 ] ) ; 
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				                    Debug . Log ( $"{nameof(MuzikaGromche)} Track {track.Name} {track.LoadedStart.length:N4} {track.LoadedLoop.length:N4}" ) ; 
 
		
	
		
			
				#endif  
		
	
		
			
				                } 
 
		
	
		
			
				                Config  =  new  Config ( base . Config ) ; 
 
		
	
		
			
				                DiscoBallManager . Load ( ) ; 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -825,11 +857,15 @@ namespace MuzikaGromche
 
		
	
		
			
				        // Beat relative to the popup. Always less than LoopBeats. When not IsLooping, can be unbounded negative. 
 
		
	
		
			
				        public  readonly  float  Beat ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  BeatTimestamp ( int  loopBeats ,  bool  isLooping ,  float  beat ) 
 
		
	
		
			
				        // Additional metadata describing whether this timestamp is based on extrapolated source data. 
 
		
	
		
			
				        public  readonly  bool  IsExtrapolated ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  BeatTimestamp ( int  loopBeats ,  bool  isLooping ,  float  beat ,  bool  isExtrapolated ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            LoopBeats  =  loopBeats ; 
 
		
	
		
			
				            IsLooping  =  isLooping  | |  beat  > =  HalfLoopBeats ; 
 
		
	
		
			
				            Beat  =  isLooping  | |  beat  > =  LoopBeats  ?  Mod . Positive ( beat ,  LoopBeats )  :  beat ; 
 
		
	
		
			
				            IsExtrapolated  =  isExtrapolated ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        public  static  BeatTimestamp  operator  + ( BeatTimestamp  self ,  float  delta ) 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -842,7 +878,7 @@ namespace MuzikaGromche
 
		
	
		
			
				                // Shouldn't be needed though, as deltas are usually short enough. 
 
		
	
		
			
				                // But don't try to chain many short negative deltas! 
 
		
	
		
			
				            } 
 
		
	
		
			
				            return  new  BeatTimestamp ( self . LoopBeats ,  self . IsLooping ,  self . Beat  +  delta ; 
 
		
	
		
			
				            return  new  BeatTimestamp ( self . LoopBeats ,  self . IsLooping ,  self . Beat  +  delta , self . IsExtrapolated  ); 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        public  static  BeatTimestamp  operator  - ( BeatTimestamp  self ,  float  delta ) 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -854,12 +890,12 @@ namespace MuzikaGromche
 
		
	
		
			
				        { 
 
		
	
		
			
				            // There is no way it wraps or affects IsLooping state 
 
		
	
		
			
				            var  beat  =  Mathf . Floor ( Beat ) ; 
 
		
	
		
			
				            return  new  BeatTimestamp ( LoopBeats ,  IsLooping ,  beat ; 
 
		
	
		
			
				            return  new  BeatTimestamp ( LoopBeats ,  IsLooping ,  beat , IsExtrapolated  ); 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        public  readonly  override  string  ToString ( ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            return  $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')}  {Beat:N4}/{LoopBeats})"; 
 
		
	
		
			
				            return  $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')} {(IsExtrapolated ? 'E' : '_')}  {Beat:N4}/{LoopBeats})"; 
 
		
	
		
			
				        } 
 
		
	
		
			
				    } 
 
		
	
		
			
				
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -872,13 +908,16 @@ namespace MuzikaGromche
 
		
	
		
			
				        public  readonly  float  BeatFromExclusive ; 
 
		
	
		
			
				        // Closed upper bound 
 
		
	
		
			
				        public  readonly  float  BeatToInclusive ; 
 
		
	
		
			
				        // Additional metadata describing whether this timestamp is based on extrapolated source data. 
 
		
	
		
			
				        public  readonly  bool  IsExtrapolated ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  BeatTimeSpan ( int  loopBeats ,  bool  isLooping ,  float  beatFromExclusive ,  float  beatToInclusive ) 
 
		
	
		
			
				        public  BeatTimeSpan ( int  loopBeats ,  bool  isLooping ,  float  beatFromExclusive ,  float  beatToInclusive , bool  isExtrapolated  )
 
		
	
		
			
				        { 
 
		
	
		
			
				            LoopBeats  =  loopBeats ; 
 
		
	
		
			
				            IsLooping  =  isLooping  | |  beatToInclusive  > =  HalfLoopBeats ; 
 
		
	
		
			
				            BeatFromExclusive  =  wrap ( beatFromExclusive ) ; 
 
		
	
		
			
				            BeatToInclusive  =  wrap ( beatToInclusive ) ; 
 
		
	
		
			
				            IsExtrapolated  =  isExtrapolated ; 
 
		
	
		
			
				
 
		
	
		
			
				            float  wrap ( float  beat ) 
 
		
	
		
			
				            { 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -888,20 +927,20 @@ namespace MuzikaGromche
 
		
	
		
			
				
 
		
	
		
			
				        public  static  BeatTimeSpan  Between ( BeatTimestamp  timestampFromExclusive ,  BeatTimestamp  timestampToInclusive ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            return  new  BeatTimeSpan ( timestampToInclusive . LoopBeats ,  timestampToInclusive . IsLooping ,  timestampFromExclusive . Beat ,  timestampToInclusive . Beat ) ; 
 
		
	
		
			
				            var  isExtrapolated  =  timestampFromExclusive . IsExtrapolated  | |  timestampToInclusive . IsExtrapolated ; 
 
		
	
		
			
				            return  new  BeatTimeSpan ( timestampToInclusive . LoopBeats ,  timestampToInclusive . IsLooping ,  timestampFromExclusive . Beat ,  timestampToInclusive . Beat ,  isExtrapolated ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				
 
		
	
		
			
				        public  static  BeatTimeSpan  Between ( float  beatFromExclusive ,  BeatTimestamp  timestampToInclusive ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            return  new  BeatTimeSpan ( timestampToInclusive . LoopBeats ,  timestampToInclusive . IsLooping ,  beatFromExclusive ,  timestampToInclusive . Beat ; 
 
		
	
		
			
				            return  new  BeatTimeSpan ( timestampToInclusive . LoopBeats ,  timestampToInclusive . IsLooping ,  beatFromExclusive ,  timestampToInclusive . Beat , timestampToInclusive . IsExtrapolated  ); 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        public  static  BeatTimeSpan  Empty  =  new ( ) ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  readonly  BeatTimestamp  ToTimestamp ( ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            return  new ( LoopBeats ,  IsLooping ,  BeatToInclusive ; 
 
		
	
		
			
				            return  new ( LoopBeats ,  IsLooping ,  BeatToInclusive , IsExtrapolated  ); 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        // The beat will not be wrapped. 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -923,7 +962,7 @@ namespace MuzikaGromche
 
		
	
		
			
				                // before wrapping (happens earlier) and after wrapping (happens later). 
 
		
	
		
			
				
 
		
	
		
			
				                // Check the "happens later" part first. 
 
		
	
		
			
				                var  laterSpan  =  new  BeatTimeSpan ( LoopBeats ,  isLooping :  false ,  beatFromExclusive :  /* epsilon to make zero inclusive */  - 0.001f ,  beatToInclusive :  BeatToInclusive ; 
 
		
	
		
			
				                var  laterSpan  =  new  BeatTimeSpan ( LoopBeats ,  isLooping :  false ,  beatFromExclusive :  /* epsilon to make zero inclusive */  - 0.001f ,  beatToInclusive :  BeatToInclusive , IsExtrapolated  ); 
 
		
	
		
			
				                var  laterIndex  =  laterSpan . GetLastIndex ( timeSeries ) ; 
 
		
	
		
			
				                if  ( laterIndex  ! =  null ) 
 
		
	
		
			
				                { 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1009,94 +1048,144 @@ namespace MuzikaGromche
 
		
	
		
			
				
 
		
	
		
			
				        public  readonly  override  string  ToString ( ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            return  $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')} , {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty ! " : " ")})" ; 
 
		
	
		
			
				            return  $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')} {(IsExtrapolated ? 'E' : '_')} , {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty ! " : " ")})" ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				    } 
 
		
	
		
			
				
 
		
	
		
			
				    class  BeatTim eState
 
		
	
		
			
				    class  ExtrapolatedAudioSourc eState
 
		
	
		
			
				    { 
 
		
	
		
			
				        //  The object is newly created, the Start audio began to play but its time hasn't adjanced from 0.0f yet. 
 
		
	
		
			
				        p rivate bool  hasStarted  =  false ;  
 
		
	
		
			
				        //  AudioSource.isPlaying 
 
		
	
		
			
				        p ublic bool  IsPlaying  {  get ;  private  set ;  }  
 
		
	
		
			
				
 
		
	
		
			
				        //  The time span after Zero state (when the Start audio has advanced from 0.0f) but before the WindUpTimer+Loop/2. 
 
		
	
		
			
				        p rivate bool  windUpOffsetIsLooping  =  fals  e; 
 
		
	
		
			
				        //  AudioSource.time, possibly extrapolated 
 
		
	
		
			
				        p ublic float  Time  = >  ExtrapolatedTim  e; 
 
		
	
		
			
				
 
		
	
		
			
				        // The time span after Zero state (when the Start audio has advanced from 0.0f) but before the WindUpTimer+LoopOffset+Loop/2. 
 
		
	
		
			
				        private  bool  loopOffsetIsLooping  =  false ; 
 
		
	
		
			
				        // The object is newly created, the AudioSource began to play (possibly delayed) but its time hasn't advanced from 0.0f yet. 
 
		
	
		
			
				        // Time can not be extrapolated when HasStarted is false. 
 
		
	
		
			
				        public  bool  HasStarted  {  get ;  private  set ;  }  =  false ; 
 
		
	
		
			
				
 
		
	
		
			
				        p rivate bool  windUpZeroBeatEventTriggered  =  fals  e; 
 
		
	
		
			
				        p ublic bool  IsExtrapolated  = >  LastKnownNonExtrapolatedTime  ! =  ExtrapolatedTim  e; 
 
		
	
		
			
				
 
		
	
		
			
				        private  readonly  Track  track ; 
 
		
	
		
			
				        private  float  ExtrapolatedTime  =  0f ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  float  loopOffsetBeat =  float . NegativeInfinity  ; 
 
		
	
		
			
				        private  float  LastKnownNonExtrapolatedTime =  0f  ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  static  System . Random  lyricsRandom  =  null ! ; 
 
		
	
		
			
				        // Any wall clock based measurements of when this state was recorded 
 
		
	
		
			
				        private  float  LastKnownRealtime  =  0f ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  int  lyricsRandomPerLoop ; 
 
		
	
		
			
				        private  const  float  MaxExtrapolationInterval  =  0.5f ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  BeatTimeState ( Track  track ) 
 
		
	
		
			
				        public  void  Update ( AudioSource  audioSource ,  float  realtime ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            if  ( lyricsRandom  = =  null ) 
 
		
	
		
			
				            IsPlaying  =  audioSource . isPlaying ; 
 
		
	
		
			
				            HasStarted  | =  audioSource . time  ! =  0f ; 
 
		
	
		
			
				
 
		
	
		
			
				            if  ( LastKnownNonExtrapolatedTime  ! =  audioSource . time ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				                lyricsRandom  =  new  System . Random ( RoundManager . Instance . playersManager . randomMapSeed  +  1337 ) ; 
 
		
	
		
			
				                lyricsRandomPerLoop  =  lyricsRandom . Next ( ) ; 
 
		
	
		
			
				                LastKnownRealtime =  realtime  ; 
 
		
	
		
			
				                LastKnownNonExtrapolatedTime =  ExtrapolatedTime  =  audioSource . time  ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				            this . track  =  track ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        public  List < BaseEvent >  Update ( AudioSource  start ,  AudioSource  loop ) 
 
		
	
		
			
				            // Frames are rendering faster than AudioSource updates its playback time state 
 
		
	
		
			
				            else  if  ( IsPlaying  & &  HasStarted  & &  Config . ExtrapolateTime ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				            hasStarted  | =  start . time  ! =  0 ; 
 
		
	
		
			
				            if  ( hasStarted ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				                var  loopTimestamp  =  UpdateStateForLoopOffset ( start ,  loop ) ; 
 
		
	
		
			
				                var  loopOffsetSpan  =  BeatTimeSpan . Between ( loopOffsetBeat ,  loopTimestamp ) ; 
 
		
	
		
			
				
 
		
	
		
			
				                // Do not go back in time 
 
		
	
		
			
				                if  ( ! loopOffsetSpan . IsEmpty ( ) ) 
 
		
	
		
			
				                { 
 
		
	
		
			
				                    if  ( loopOffsetSpan . BeatFromExclusive  >  loopOffsetSpan . BeatToInclusive ) 
 
		
	
		
			
				                    { 
 
		
	
		
			
				                        lyricsRandomPerLoop  =  lyricsRandom . Next ( ) ; 
 
		
	
		
			
				                    } 
 
		
	
		
			
				
 
		
	
		
			
				                    var  windUpOffsetTimestamp  =  UpdateStateForWindUpOffset ( start ,  loop ) ; 
 
		
	
		
			
				                    loopOffsetBeat  =  loopTimestamp . Beat ; 
 
		
	
		
			
				                    var  events  =  GetEvents ( loopOffsetSpan ,  windUpOffsetTimestamp ) ; 
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				                    Debug . Log ( $"{nameof(MuzikaGromche)} looping? {(loopOffsetIsLooping ? 'X' : '_')}{(windUpOffsetIsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} events={string.Join(" , ", events)}" ) ;  
 
		
	
		
			
				                Debug . Assert ( LastKnownNonExtrapolatedTime  = =  audioSource . time ) ;  // implied 
 
		
	
		
			
				#endif  
		
	
		
			
				                    return  events ; 
 
		
	
		
			
				                } 
 
		
	
		
			
				            } 
 
		
	
		
			
				
 
		
	
		
			
				            return  [ ] ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        // Events other than colors start rotating at 0=WindUpTimer+LoopOffset. 
 
		
	
		
			
				        private  BeatTimestamp  UpdateStateForLoopOffset ( AudioSource  start ,  AudioSource  loop ) 
 
		
	
		
			
				                var  deltaTime  =  realtime  -  LastKnownRealtime ; 
 
		
	
		
			
				                if  ( 0  <  deltaTime  & &  deltaTime  <  MaxExtrapolationInterval ) 
 
		
	
		
			
				                { 
 
		
	
		
			
				            var  offset  =  BaseOffset ( )  +  track . LoopOffsetInSeconds ; 
 
		
	
		
			
				            var  timestamp  =  GetTimestampRelativeToGivenOffset ( start ,  loop ,  offset ,  loopOffsetIsLooping ) ; 
 
		
	
		
			
				            loopOffsetIsLooping  | =  timestamp . IsLooping ; 
 
		
	
		
			
				            return  timestamp ; 
 
		
	
		
			
				                    ExtrapolatedTime  =  LastKnownNonExtrapolatedTime  +  deltaTime ; 
 
		
	
		
			
				                } 
 
		
	
		
			
				            } 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        // Colors start rotating at 0=WindUpTimer 
 
		
	
		
			
				        private  BeatTimestamp  UpdateStateForWindUpOffset ( AudioSource  start ,  AudioSource  loop ) 
 
		
	
		
			
				        public  void  Finish ( ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            var  offset  =  BaseOffset ( ) ; 
 
		
	
		
			
				            var  timestamp  =  GetTimestampRelativeToGivenOffset ( start ,  loop ,  offset ,  windUpOffsetIsLooping ) ; 
 
		
	
		
			
				            windUpOffsetIsLooping  | =  timestamp . IsLooping ; 
 
		
	
		
			
				            return  timestamp ; 
 
		
	
		
			
				            IsPlaying  =  false ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        p rivate float  BaseOffset  ( ) 
 
		
	
		
			
				        public  override  string  ToString ( ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            return  Config . AudioOffset . Value  +  track . BeatsOffsetInSeconds  +  track . WindUpTimer ; 
 
		
	
		
			
				            return  $"{nameof(ExtrapolatedAudioSourceState)}({(IsPlaying ? 'P' : '_')}{(HasStarted ? 'S' : '0')} " 
 
		
	
		
			
				                +  ( IsExtrapolated 
 
		
	
		
			
				                    ?  $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4} => {ExtrapolatedTime:N4}" 
 
		
	
		
			
				                    :  $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4}" 
 
		
	
		
			
				                )  +  ")" ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				    } 
 
		
	
		
			
				
 
		
	
		
			
				        BeatTimestamp  GetTimestampRelativeToGivenOffset ( AudioSource  start ,  AudioSource  loop ,  float  offset ,  bool  isLooping ) 
 
		
	
		
			
				    class  JesterAudioSourcesState 
 
		
	
		
			
				    { 
 
		
	
		
			
				        private  readonly  float  StartClipLength ; 
 
		
	
		
			
				
 
		
	
		
			
				        // Neither start.isPlaying or loop.isPlaying are reliable indicators of which track is actually playing right now: 
 
		
	
		
			
				        // start.isPlaying would be true during the loop when Jester chases a player, 
 
		
	
		
			
				        // loop.isPlaying would be true when it is played delyaed but hasn't actually started playing yet. 
 
		
	
		
			
				        private  readonly  ExtrapolatedAudioSourceState  Start  =  new ( ) ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  readonly  ExtrapolatedAudioSourceState  Loop  =  new ( ) ; 
 
		
	
		
			
				
 
		
	
		
			
				        // If true, use Start state as a reference, otherwise use Loop. 
 
		
	
		
			
				        private  bool  ReferenceIsStart  =  true ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  bool  HasStarted  = >  Start . HasStarted ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  bool  IsExtrapolated  = >  ReferenceIsStart  ?  Start . IsExtrapolated  :  Loop . IsExtrapolated ; 
 
		
	
		
			
				
 
		
	
		
			
				        // Time from the start of the start clip. It wraps when the loop AudioSource loops: 
 
		
	
		
			
				        // [...start...][...loop...] 
 
		
	
		
			
				        //              ^          | 
 
		
	
		
			
				        //              `----------' 
 
		
	
		
			
				        public  float  Time  = >  ReferenceIsStart 
 
		
	
		
			
				            ?  Start . Time 
 
		
	
		
			
				            :  StartClipLength  +  Loop . Time ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  JesterAudioSourcesState ( float  startClipLength ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            StartClipLength  =  startClipLength ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        public  void  Update ( AudioSource  start ,  AudioSource  loop ,  float  realtime ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            // It doesn't make sense to update start state after loop has started (because start.isPlaying occasionally becomes true). 
 
		
	
		
			
				            // But always makes sense to update loop, so we can check if it has actually started. 
 
		
	
		
			
				            Loop . Update ( loop ,  realtime ) ; 
 
		
	
		
			
				
 
		
	
		
			
				            if  ( ! Loop . HasStarted ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				                Debug . Assert ( ReferenceIsStart ) ; 
 
		
	
		
			
				#endif  
		
	
		
			
				                Start . Update ( start ,  realtime ) ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				            else 
 
		
	
		
			
				            { 
 
		
	
		
			
				                ReferenceIsStart  =  false ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				        } 
 
		
	
		
			
				    } 
 
		
	
		
			
				
 
		
	
		
			
				    // This class tracks looping state of the playback, so that the timestamps can be correctly wrapped only when needed. 
 
		
	
		
			
				    // [... ...time... ...] 
 
		
	
		
			
				    //            ^       | 
 
		
	
		
			
				    //            `---|---' loop 
 
		
	
		
			
				    //                ^ IsLooping becomes true and stays true forever. 
 
		
	
		
			
				    class  AudioLoopingState 
 
		
	
		
			
				    { 
 
		
	
		
			
				        public  bool  IsLooping  {  get ;  private  set ;  }  =  false ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  readonly  float  StartOfLoop ; 
 
		
	
		
			
				        private  readonly  float  LoopLength ; 
 
		
	
		
			
				        private  readonly  int  Beats ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  AudioLoopingState ( float  startOfLoop ,  float  loopLength ,  int  beats ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            StartOfLoop  =  startOfLoop ; 
 
		
	
		
			
				            LoopLength  =  loopLength ; 
 
		
	
		
			
				            Beats  =  beats ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        public  BeatTimestamp  Update ( float  time ,  bool  isExtrapolated ,  float  additionalOffset ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            // If popped, calculate which beat the music is currently at. 
 
		
	
		
			
				            // In order to do that we should choose one of two strategies: 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1109,42 +1198,114 @@ namespace MuzikaGromche
 
		
	
		
			
				            // NOTE 2: There is a weird state when Jester has popped and chases a player: 
 
		
	
		
			
				            //         Start/farAudio isPlaying is true but stays exactly at zero time, so we need to ignore that. 
 
		
	
		
			
				
 
		
	
		
			
				            var  timeFromTheVeryStart  =  start . isPlaying  & &  start . time  ! =  0f 
 
		
	
		
			
				                // [1] Start source is still playing 
 
		
	
		
			
				                ?  start . time 
 
		
	
		
			
				                // [2] Start source has finished 
 
		
	
		
			
				                :  track . LoadedStart . length  +  loop . time ; 
 
		
	
		
			
				            var  offset  =  StartOfLoop  +  additionalOffset ; 
 
		
	
		
			
				
 
		
	
		
			
				            float  adjustedTimeFromOffset =  timeFromTheVeryStart   -  offset ; 
 
		
	
		
			
				            float  timeSinceStartOfLoop  =  time  -  offset ; 
 
		
	
		
			
				
 
		
	
		
			
				            var  adjustedTimeNormalized  =  adjustedTimeFromOffset /  track . LoadedLoop . l  ength; 
 
		
	
		
			
				            var  adjustedTimeNormalized  =  timeSinceStartOfLoop  /  LoopLength ; 
 
		
	
		
			
				
 
		
	
		
			
				            var  beat  =  adjustedTimeNormalized  *  track.  Beats; 
 
		
	
		
			
				            var  beat  =  adjustedTimeNormalized  *  ; 
 
		
	
		
			
				
 
		
	
		
			
				            // Let it infer the isLooping flag from the beat 
 
		
	
		
			
				            var  timestamp  =  new  BeatTimestamp ( track . Beats ,  isLooping ,  beat ) ; 
 
		
	
		
			
				            var  timestamp  =  new  BeatTimestamp ( Beats ,  IsLooping ,  beat ,  isExtrapolated ) ; 
 
		
	
		
			
				
 
		
	
		
			
				            IsLooping  | =  timestamp . IsLooping ; 
 
		
	
		
			
				
 
		
	
		
			
				#if  DEBUG  & &  false  
		
	
		
			
				            var  color  =  ColorFromPaletteAtTimestamp ( timestamp ) ; 
 
		
	
		
			
				            Debug . LogFormat ( "{0} t={1,10:N4} d={2,7:N4} Start[{3}{4,8:N4} zero? {5}] Loop[{6}{7,8:N4}] norm={8,6:N4} beat={9,7:N4} color={10}" , 
 
		
	
		
			
				            Debug . LogFormat ( "{0} t={1,10:N4} d={2,7:N4} {3} Time={4:N4} norm={5,6:N4} beat={6,7:N4}" , 
 
		
	
		
			
				                nameof ( MuzikaGromche ) , 
 
		
	
		
			
				                Time . realtimeSinceStartup ,  Time . deltaTime , 
 
		
	
		
			
				                ( start . isPlaying  ?  '+'  :  ' ' ) ,  start . time ,  ( start . time  = =  0f  ?  'Y'  :  'n' ) , 
 
		
	
		
			
				                ( loop . isPlaying  ?  '+'  :  ' ' ) ,  loop . time , 
 
		
	
		
			
				                adjustedTimeNormalized ,  beat ,  color ) ; 
 
		
	
		
			
				                isExtrapolated  ?  'E'  :  '_' ,  time , 
 
		
	
		
			
				                adjustedTimeNormalized ,  beat ) ; 
 
		
	
		
			
				#endif  
		
	
		
			
				
 
		
	
		
			
				            return  timestamp ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				    } 
 
		
	
		
			
				
 
		
	
		
			
				    class  BeatTimeState 
 
		
	
		
			
				    { 
 
		
	
		
			
				        private  readonly  Track  track ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  readonly  JesterAudioSourcesState  AudioState ; 
 
		
	
		
			
				
 
		
	
		
			
				        // Colors wrap from WindUpTimer 
 
		
	
		
			
				        private  readonly  AudioLoopingState  WindUpLoopingState ; 
 
		
	
		
			
				
 
		
	
		
			
				        // Events other than colors wrap from WindUpTimer+LoopOffset. 
 
		
	
		
			
				        private  readonly  AudioLoopingState  LoopLoopingState ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  float  LastKnownLoopOffsetBeat  =  float . NegativeInfinity ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  static  System . Random  LyricsRandom  =  null ! ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  int  LyricsRandomPerLoop ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  bool  WindUpZeroBeatEventTriggered  =  false ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  BeatTimeState ( Track  track ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            if  ( LyricsRandom  = =  null ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				                LyricsRandom  =  new  System . Random ( RoundManager . Instance . playersManager . randomMapSeed  +  1337 ) ; 
 
		
	
		
			
				                LyricsRandomPerLoop  =  LyricsRandom . Next ( ) ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				            this . track  =  track ; 
 
		
	
		
			
				            AudioState  =  new ( track . LoadedStart . length ) ; 
 
		
	
		
			
				            WindUpLoopingState  =  new ( track . WindUpTimer ,  track . LoadedLoop . length ,  track . Beats ) ; 
 
		
	
		
			
				            LoopLoopingState  =  new ( track . WindUpTimer  +  track . LoopOffsetInSeconds ,  track . LoadedLoop . length ,  track . Beats ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        public  List < BaseEvent >  Update ( AudioSource  start ,  AudioSource  loop ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            var  time  =  Time . realtimeSinceStartup ; 
 
		
	
		
			
				            AudioState . Update ( start ,  loop ,  time ) ; 
 
		
	
		
			
				
 
		
	
		
			
				            if  ( AudioState . HasStarted ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				                var  loopTimestamp  =  Update ( LoopLoopingState ) ; 
 
		
	
		
			
				                var  loopOffsetSpan  =  BeatTimeSpan . Between ( LastKnownLoopOffsetBeat ,  loopTimestamp ) ; 
 
		
	
		
			
				
 
		
	
		
			
				                // Do not go back in time 
 
		
	
		
			
				                if  ( ! loopOffsetSpan . IsEmpty ( ) ) 
 
		
	
		
			
				                { 
 
		
	
		
			
				                    if  ( loopOffsetSpan . BeatFromExclusive  >  loopOffsetSpan . BeatToInclusive ) 
 
		
	
		
			
				                    { 
 
		
	
		
			
				                        LyricsRandomPerLoop  =  LyricsRandom . Next ( ) ; 
 
		
	
		
			
				                    } 
 
		
	
		
			
				
 
		
	
		
			
				                    var  windUpOffsetTimestamp  =  Update ( WindUpLoopingState ) ; 
 
		
	
		
			
				                    LastKnownLoopOffsetBeat  =  loopTimestamp . Beat ; 
 
		
	
		
			
				                    var  events  =  GetEvents ( loopOffsetSpan ,  windUpOffsetTimestamp ) ; 
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				                    Debug . Log ( $"{nameof(MuzikaGromche)} looping? {(LoopLoopingState.IsLooping ? 'X' : '_')}{(WindUpLoopingState.IsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} Time={Time.realtimeSinceStartup:N4} events={string.Join(" , ", events)}" ) ; 
 
		
	
		
			
				#endif  
		
	
		
			
				                    return  events ; 
 
		
	
		
			
				                } 
 
		
	
		
			
				            } 
 
		
	
		
			
				
 
		
	
		
			
				            return  [ ] ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        private  BeatTimestamp  Update ( AudioLoopingState  loopingState ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            return  loopingState . Update ( AudioState . Time ,  AudioState . IsExtrapolated ,  AdditionalOffset ( ) ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        // Timings that may be changes through config 
 
		
	
		
			
				        private  float  AdditionalOffset ( ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            return  Config . AudioOffset . Value  +  track . BeatsOffsetInSeconds ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        private  List < BaseEvent >  GetEvents ( BeatTimeSpan  loopOffsetSpan ,  BeatTimestamp  windUpOffsetTimestamp ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            List < BaseEvent >  events  =  [ ] ; 
 
		
	
		
			
				
 
		
	
		
			
				            if  ( windUpOffsetTimestamp . Beat  > =  0f  & &  ! windUpZeroBeatEventTriggered ) 
 
		
	
		
			
				            if  ( windUpOffsetTimestamp . Beat  > =  0f  & &  ! W indUpZeroBeatEventTriggered) 
 
		
	
		
			
				            { 
 
		
	
		
			
				                events . Add ( new  WindUpZeroBeatEvent ( ) ) ; 
 
		
	
		
			
				                windUpZeroBeatEventTriggered  =  true ; 
 
		
	
		
			
				                W indUpZeroBeatEventTriggered =  true ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				
 
		
	
		
			
				            if  ( GetColorEvent ( loopOffsetSpan ,  windUpOffsetTimestamp )  is  {  }  colorEvent ) 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1166,7 +1327,7 @@ namespace MuzikaGromche
 
		
	
		
			
				                { 
 
		
	
		
			
				                    var  line  =  track . LyricsLines [ i ] ; 
 
		
	
		
			
				                    var  alternatives  =  line . Split ( '\t' ) ; 
 
		
	
		
			
				                    var  randomIndex  =  l yricsRandomPerLoop %  alternatives . Length ; 
 
		
	
		
			
				                    var  randomIndex  =  L yricsRandomPerLoop %  alternatives . Length ; 
 
		
	
		
			
				                    var  alternative  =  alternatives [ randomIndex ] ; 
 
		
	
		
			
				                    if  ( alternative  ! =  "" ) 
 
		
	
		
			
				                    { 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1433,6 +1594,7 @@ namespace MuzikaGromche
 
		
	
		
			
				        readonly  public  int  TotalWeights  {  get ;  } 
 
		
	
		
			
				    } 
 
		
	
		
			
				
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				    static  class  SyncedEntryExtensions 
 
		
	
		
			
				    { 
 
		
	
		
			
				        // Update local values on clients. Even though the clients couldn't 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1445,8 +1607,12 @@ namespace MuzikaGromche
 
		
	
		
			
				            } ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				    } 
 
		
	
		
			
				#endif  
		
	
		
			
				
 
		
	
		
			
				    class  Config  :  SyncedConfig2 < Config > 
 
		
	
		
			
				    class  Config 
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				        :  SyncedConfig2 < Config > 
 
		
	
		
			
				#endif  
		
	
		
			
				    { 
 
		
	
		
			
				        public  static  ConfigEntry < bool >  DisplayLyrics  {  get ;  private  set ;  }  =  null ! ; 
 
		
	
		
			
				
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1456,6 +1622,7 @@ namespace MuzikaGromche
 
		
	
		
			
				
 
		
	
		
			
				        public  static  ConfigEntry < bool >  OverrideSpawnRates  {  get ;  private  set ;  }  =  null ! ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  static  bool  ExtrapolateTime  {  get ;  private  set ;  }  =  true ; 
 
		
	
		
			
				        public  static  bool  ShouldSkipWindingPhase  {  get ;  private  set ;  }  =  false ; 
 
		
	
		
			
				
 
		
	
		
			
				        public  static  Palette ?  PaletteOverride  {  get ;  private  set ;  }  =  null ; 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1469,7 +1636,10 @@ namespace MuzikaGromche
 
		
	
		
			
				        public  static  float?  ColorTransitionOutOverride  {  get ;  private  set ;  }  =  null ; 
 
		
	
		
			
				        public  static  string?  ColorTransitionEasingOverride  {  get ;  private  set ;  }  =  null ; 
 
		
	
		
			
				
 
		
	
		
			
				        internal  Config ( ConfigFile  configFile )  :  base ( PluginInfo . PLUGIN_GUID ) 
 
		
	
		
			
				        internal  Config ( ConfigFile  configFile ) 
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				            :  base ( PluginInfo . PLUGIN_GUID ) 
 
		
	
		
			
				#endif  
		
	
		
			
				        { 
 
		
	
		
			
				            DisplayLyrics  =  configFile . Bind ( "General" ,  "Display Lyrics" ,  true , 
 
		
	
		
			
				                new  ConfigDescription ( "Display lyrics in the HUD tooltip when you hear the music." ) ) ; 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1489,6 +1659,7 @@ namespace MuzikaGromche
 
		
	
		
			
				            LethalConfigManager . AddConfigItem ( new  BoolCheckBoxConfigItem ( OverrideSpawnRates ,  Default ( new  BoolCheckBoxOptions ( ) ) ) ) ; 
 
		
	
		
			
				
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				            SetupEntriesForExtrapolation ( configFile ) ; 
 
		
	
		
			
				            SetupEntriesToSkipWinding ( configFile ) ; 
 
		
	
		
			
				            SetupEntriesForPaletteOverride ( configFile ) ; 
 
		
	
		
			
				            SetupEntriesForTimingsOverride ( configFile ) ; 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1537,9 +1708,12 @@ namespace MuzikaGromche
 
		
	
		
			
				                LethalConfigManager . AddConfigItem ( new  IntSliderConfigItem ( track . Weight ,  Default ( new  IntSliderOptions ( ) ) ) ) ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				            ConfigManager . Register ( this ) ; 
 
		
	
		
			
				#endif  
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				        // HACK because CSync doesn't provide an API to register a list of config entries 
 
		
	
		
			
				        // See https://github.com/lc-sigurd/CSync/issues/11 
 
		
	
		
			
				        private  void  CSyncHackAddSyncedEntry ( SyncedEntryBase  entryBase ) 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1547,6 +1721,7 @@ namespace MuzikaGromche
 
		
	
		
			
				            // This is basically what ConfigFile.PopulateEntryContainer does 
 
		
	
		
			
				            EntryContainer . Add ( entryBase . BoxedEntry . ToSyncedEntryIdentifier ( ) ,  entryBase ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				#endif  
		
	
		
			
				
 
		
	
		
			
				        public  static  CanModifyResult  CanModifyIfHost ( ) 
 
		
	
		
			
				        { 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1582,6 +1757,23 @@ namespace MuzikaGromche
 
		
	
		
			
				            return  CanModifyResult . True ( ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				#if  DEBUG  
		
	
		
			
				        private  void  SetupEntriesForExtrapolation ( ConfigFile  configFile ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            var  syncedEntry  =  configFile . BindSyncedEntry ( "General" ,  "Extrapolate Audio Playback Time" ,  true , 
 
		
	
		
			
				                new  ConfigDescription ( "AudioSource only updates its playback position about 20 times per second.\n\nUse extrapolation technique to predict playback time between updates for smoother color animations." ) ) ; 
 
		
	
		
			
				            LethalConfigManager . AddConfigItem ( new  BoolCheckBoxConfigItem ( syncedEntry . Entry ,  Default ( new  BoolCheckBoxOptions ( ) ) ) ) ; 
 
		
	
		
			
				            CSyncHackAddSyncedEntry ( syncedEntry ) ; 
 
		
	
		
			
				            syncedEntry . Changed  + =  ( sender ,  args )  = >  apply ( ) ; 
 
		
	
		
			
				            syncedEntry . SyncHostToLocal ( ) ; 
 
		
	
		
			
				            apply ( ) ; 
 
		
	
		
			
				
 
		
	
		
			
				            void  apply ( ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				                ExtrapolateTime  =  syncedEntry . Value ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        private  void  SetupEntriesToSkipWinding ( ConfigFile  configFile ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            var  syncedEntry  =  configFile . BindSyncedEntry ( "General" ,  "Skip Winding Phase" ,  false , 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1693,7 +1885,7 @@ namespace MuzikaGromche
 
		
	
		
			
				            fadeOutBeatSyncedEntry  =  configFile . BindSyncedEntry ( section ,  "Fade Out Beat" ,  0f , 
 
		
	
		
			
				                new  ConfigDescription ( "The beat at which to start fading out" ,  new  AcceptableValueRange < float > ( - 1000f ,  0 ) ) ) ; 
 
		
	
		
			
				            fadeOutDurationSyncedEntry  =  configFile . BindSyncedEntry ( section ,  "Fade Out Duration" ,  0f , 
 
		
	
		
			
				                new  ConfigDescription ( "Duration of fading out" ,  new  AcceptableValueRange < float > ( 0 ,  10 0 ) ) ) ; 
 
		
	
		
			
				                new  ConfigDescription ( "Duration of fading out" ,  new  AcceptableValueRange < float > ( 0 ,  10 ) ) ) ; 
 
		
	
		
			
				            flickerLightsTimeSeriesSyncedEntry  =  configFile . BindSyncedEntry ( section ,  "Flicker Lights Time Series" ,  "" , 
 
		
	
		
			
				                new  ConfigDescription ( "Time series of beat offsets when to flicker the lights." ) ) ; 
 
		
	
		
			
				            lyricsTimeSeriesSyncedEntry  =  configFile . BindSyncedEntry ( section ,  "Lyrics Time Series" ,  "" , 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1789,6 +1981,7 @@ namespace MuzikaGromche
 
		
	
		
			
				                } 
 
		
	
		
			
				            } 
 
		
	
		
			
				        } 
 
		
	
		
			
				#endif  
		
	
		
			
				
 
		
	
		
			
				        private  T  Default < T > ( T  options )  where  T  :  BaseOptions 
 
		
	
		
			
				        { 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1828,15 +2021,30 @@ namespace MuzikaGromche
 
		
	
		
			
				            ChooseTrackDeferred ( ) ; 
 
		
	
		
			
				            foreach  ( var  track  in  Plugin . Tracks ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				                track . Weight . SettingChanged  + =  ( _ ,  _ )  = >  ChooseTrackDeferred ( ) ; 
 
		
	
		
			
				                track . Weight . SettingChanged  + =  ChooseTrackDeferredDelegate ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				            Config . SkipExplicitTracks . SettingChanged  + =  ( _ ,  _ )  = >  ChooseTrackDeferred ( ) ; 
 
		
	
		
			
				            Config . SkipExplicitTracks . SettingChanged  + =  ChooseTrackDeferredDelegate ; 
 
		
	
		
			
				            base . OnNetworkSpawn ( ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        public  override  void  OnNetworkDespawn ( ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            foreach  ( var  track  in  Plugin . Tracks ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				                track . Weight . SettingChanged  - =  ChooseTrackDeferredDelegate ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				            Config . SkipExplicitTracks . SettingChanged  - =  ChooseTrackDeferredDelegate ; 
 
		
	
		
			
				            base . OnNetworkDespawn ( ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        // Batch multiple weights changes in a single network RPC 
 
		
	
		
			
				        private  Coroutine ?  DeferredCoroutine  =  null ; 
 
		
	
		
			
				
 
		
	
		
			
				        private  void  ChooseTrackDeferredDelegate ( object  sender ,  EventArgs  e ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            ChooseTrackDeferred ( ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				
 
		
	
		
			
				        private  void  ChooseTrackDeferred ( ) 
 
		
	
		
			
				        { 
 
		
	
		
			
				            if  ( DeferredCoroutine  ! =  null ) 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1968,7 +2176,7 @@ namespace MuzikaGromche
 
		
	
		
			
				                __instance . farAudio  =  __state . farAudio ; 
 
		
	
		
			
				
 
		
	
		
			
				                var  time  =  __instance . farAudio . time ; 
 
		
	
		
			
				                var  delay  =  Plugin . CurrentTrack ! .LoadedStart . length  -  time ; 
 
		
	
		
			
				                var  delay  =  Plugin . CurrentTrack LoadedStart . length  -  time ; 
 
		
	
		
			
				
 
		
	
		
			
				                // Override screamingSFX with Loop, delayed by the remaining time of the Start audio 
 
		
	
		
			
				                __instance . creatureVoice . Stop ( ) ; 
 
		
	
	
		
			
				
					
						
					 
				
			
			@ -1981,9 +2189,9 @@ namespace MuzikaGromche
 
		
	
		
			
				            } 
 
		
	
		
			
				
 
		
	
		
			
				            // Manage the timeline: switch color of the lights according to the current playback/beat position. 
 
		
	
		
			
				            if  ( ( __instance . previousState  = =  1  | |  __instance . previousState  = =  2 )  & &  Plugin . BeatTimeState  ! =  null ) 
 
		
	
		
			
				            if  ( ( __instance . previousState  = =  1  | |  __instance . previousState  = =  2 )  & &  Plugin . BeatTimeState  is  {  }  beatTimeState ) 
 
		
	
		
			
				            { 
 
		
	
		
			
				                var  events  =  Plugin. B  eatTimeState. Update ( start :  __instance . farAudio ,  loop :  __instance . creatureVoice ) ; 
 
		
	
		
			
				                var  events  =  b eatTimeState. Update ( start :  __instance . farAudio ,  loop :  __instance . creatureVoice ) ; 
 
		
	
		
			
				                foreach  ( var  ev  in  events ) 
 
		
	
		
			
				                { 
 
		
	
		
			
				                    switch  ( ev )