forked from nikita/muzika-gromche
				
			Compare commits
	
		
			No commits in common. "df796965f2b9f77a64d03955a5d3387bbf5ed675" and "8b2f4428bb97118b42d9c6e7c4dcd4482846dbdd" have entirely different histories.
		
	
	
		
			df796965f2
			...
			8b2f4428bb
		
	
		|  | @ -1,5 +0,0 @@ | ||||||
| [*.cs] |  | ||||||
| 
 |  | ||||||
| # IDE0290: Use primary constructor |  | ||||||
| # Primary constructors are far from perfect: they can't have readonly fields, while fields can be used anywhere in the class body. |  | ||||||
| csharp_style_prefer_primary_constructors = false |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								Assets/BeefLiverLoop.ogg (Stored with Git LFS)
								
								
								
								
							
							
						
						
									
										
											BIN
										
									
								
								Assets/BeefLiverLoop.ogg (Stored with Git LFS)
								
								
								
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								Assets/BeefLiverStart.ogg (Stored with Git LFS)
								
								
								
								
							
							
						
						
									
										
											BIN
										
									
								
								Assets/BeefLiverStart.ogg (Stored with Git LFS)
								
								
								
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -1,11 +1,5 @@ | ||||||
| # Changelog | # Changelog | ||||||
| 
 | 
 | ||||||
| ## MuzikaGromche 1337.420.69 - It's All DiscoNnected Edition |  | ||||||
| 
 |  | ||||||
| - Fix harmless but annoying errors in BepInEx console output. |  | ||||||
| - Improve smoothness of color animations. |  | ||||||
| - Add a new track. |  | ||||||
| 
 |  | ||||||
| ## MuzikaGromche 1337.69.420 - It's All Connected Edition | ## MuzikaGromche 1337.69.420 - It's All Connected Edition | ||||||
| 
 | 
 | ||||||
| - Fix certain object hanging around after being disabled. | - Fix certain object hanging around after being disabled. | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
|         <AssemblyName>Ratijas.MuzikaGromche</AssemblyName> |         <AssemblyName>Ratijas.MuzikaGromche</AssemblyName> | ||||||
|         <Product>Muzika Gromche</Product> |         <Product>Muzika Gromche</Product> | ||||||
|         <Description>Add some content to your inverse teleporter experience on Titan!</Description> |         <Description>Add some content to your inverse teleporter experience on Titan!</Description> | ||||||
|         <Version>1337.420.69</Version> |         <Version>1337.69.420</Version> | ||||||
|         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> |         <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||||||
|         <LangVersion>latest</LangVersion> |         <LangVersion>latest</LangVersion> | ||||||
|         <Nullable>enable</Nullable> |         <Nullable>enable</Nullable> | ||||||
|  | @ -41,7 +41,6 @@ | ||||||
|         <!-- |         <!-- | ||||||
|             Publicize internal methods, so we could generate config entries for tracks at runtime instead |             Publicize internal methods, so we could generate config entries for tracks at runtime instead | ||||||
|             of generating code at compile time. See https://github.com/lc-sigurd/CSync/issues/11 |             of generating code at compile time. See https://github.com/lc-sigurd/CSync/issues/11 | ||||||
|             It is an optional dependency now, but there is no sane way to mark it as such. |  | ||||||
|         --> |         --> | ||||||
|         <PackageReference Include="Sigurd.BepInEx.CSync" Version="5.0.1" Publicize="true" PrivateAssets="all" Private="false" /> |         <PackageReference Include="Sigurd.BepInEx.CSync" Version="5.0.1" Publicize="true" PrivateAssets="all" Private="false" /> | ||||||
|         <PackageReference Include="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" /> |         <PackageReference Include="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" /> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
| using BepInEx; | using BepInEx; | ||||||
| using BepInEx.Configuration; | using BepInEx.Configuration; | ||||||
|  | using CSync.Extensions; | ||||||
|  | using CSync.Lib; | ||||||
| using HarmonyLib; | using HarmonyLib; | ||||||
| using LethalConfig; | using LethalConfig; | ||||||
| using LethalConfig.ConfigItems; | using LethalConfig.ConfigItems; | ||||||
|  | @ -19,17 +21,10 @@ using Unity.Netcode; | ||||||
| using UnityEngine; | using UnityEngine; | ||||||
| using UnityEngine.Networking; | using UnityEngine.Networking; | ||||||
| 
 | 
 | ||||||
| #if DEBUG |  | ||||||
| using CSync.Extensions; |  | ||||||
| using CSync.Lib; |  | ||||||
| #endif |  | ||||||
| 
 |  | ||||||
| namespace MuzikaGromche | namespace MuzikaGromche | ||||||
| { | { | ||||||
|     [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] |     [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] | ||||||
| #if DEBUG |  | ||||||
|     [BepInDependency("com.sigurd.csync", "5.0.1")] |     [BepInDependency("com.sigurd.csync", "5.0.1")] | ||||||
| #endif |  | ||||||
|     [BepInDependency("ainavt.lc.lethalconfig", "1.4.6")] |     [BepInDependency("ainavt.lc.lethalconfig", "1.4.6")] | ||||||
|     [BepInDependency("watergun.v72lightfix", BepInDependency.DependencyFlags.SoftDependency)] |     [BepInDependency("watergun.v72lightfix", BepInDependency.DependencyFlags.SoftDependency)] | ||||||
|     [BepInDependency("BMX.LobbyCompatibility", BepInDependency.DependencyFlags.HardDependency)] |     [BepInDependency("BMX.LobbyCompatibility", BepInDependency.DependencyFlags.HardDependency)] | ||||||
|  | @ -475,27 +470,6 @@ namespace MuzikaGromche | ||||||
|                 FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45], |                 FlickerLightsTimeSeries = [-120.5f, -105, -89, -8, 44, 45], | ||||||
|                 Lyrics = [], |                 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() |         public static Track ChooseTrack() | ||||||
|  | @ -555,9 +529,6 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|         void Awake() |         void Awake() | ||||||
|         { |         { | ||||||
|             // Sort in place by name |  | ||||||
|             Array.Sort(Tracks.Select(track => track.Name).ToArray(), Tracks); |  | ||||||
| 
 |  | ||||||
|             string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); |             string dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); | ||||||
|             UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2]; |             UnityWebRequest[] requests = new UnityWebRequest[Tracks.Length * 2]; | ||||||
|             for (int i = 0; i < Tracks.Length; i++) |             for (int i = 0; i < Tracks.Length; i++) | ||||||
|  | @ -578,9 +549,6 @@ namespace MuzikaGromche | ||||||
|                     Track track = Tracks[i]; |                     Track track = Tracks[i]; | ||||||
|                     track.LoadedStart = DownloadHandlerAudioClip.GetContent(requests[i * 2]); |                     track.LoadedStart = DownloadHandlerAudioClip.GetContent(requests[i * 2]); | ||||||
|                     track.LoadedLoop = DownloadHandlerAudioClip.GetContent(requests[i * 2 + 1]); |                     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); |                 Config = new Config(base.Config); | ||||||
|                 DiscoBallManager.Load(); |                 DiscoBallManager.Load(); | ||||||
|  | @ -857,15 +825,11 @@ namespace MuzikaGromche | ||||||
|         // Beat relative to the popup. Always less than LoopBeats. When not IsLooping, can be unbounded negative. |         // Beat relative to the popup. Always less than LoopBeats. When not IsLooping, can be unbounded negative. | ||||||
|         public readonly float Beat; |         public readonly float Beat; | ||||||
| 
 | 
 | ||||||
|         // Additional metadata describing whether this timestamp is based on extrapolated source data. |         public BeatTimestamp(int loopBeats, bool isLooping, float beat) | ||||||
|         public readonly bool IsExtrapolated; |  | ||||||
| 
 |  | ||||||
|         public BeatTimestamp(int loopBeats, bool isLooping, float beat, bool isExtrapolated) |  | ||||||
|         { |         { | ||||||
|             LoopBeats = loopBeats; |             LoopBeats = loopBeats; | ||||||
|             IsLooping = isLooping || beat >= HalfLoopBeats; |             IsLooping = isLooping || beat >= HalfLoopBeats; | ||||||
|             Beat = isLooping || beat >= LoopBeats ? Mod.Positive(beat, LoopBeats) : beat; |             Beat = isLooping || beat >= LoopBeats ? Mod.Positive(beat, LoopBeats) : beat; | ||||||
|             IsExtrapolated = isExtrapolated; |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public static BeatTimestamp operator +(BeatTimestamp self, float delta) |         public static BeatTimestamp operator +(BeatTimestamp self, float delta) | ||||||
|  | @ -878,7 +842,7 @@ namespace MuzikaGromche | ||||||
|                 // Shouldn't be needed though, as deltas are usually short enough. |                 // Shouldn't be needed though, as deltas are usually short enough. | ||||||
|                 // But don't try to chain many short negative deltas! |                 // But don't try to chain many short negative deltas! | ||||||
|             } |             } | ||||||
|             return new BeatTimestamp(self.LoopBeats, self.IsLooping, self.Beat + delta, self.IsExtrapolated); |             return new BeatTimestamp(self.LoopBeats, self.IsLooping, self.Beat + delta); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public static BeatTimestamp operator -(BeatTimestamp self, float delta) |         public static BeatTimestamp operator -(BeatTimestamp self, float delta) | ||||||
|  | @ -890,12 +854,12 @@ namespace MuzikaGromche | ||||||
|         { |         { | ||||||
|             // There is no way it wraps or affects IsLooping state |             // There is no way it wraps or affects IsLooping state | ||||||
|             var beat = Mathf.Floor(Beat); |             var beat = Mathf.Floor(Beat); | ||||||
|             return new BeatTimestamp(LoopBeats, IsLooping, beat, IsExtrapolated); |             return new BeatTimestamp(LoopBeats, IsLooping, beat); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public readonly override string ToString() |         public readonly override string ToString() | ||||||
|         { |         { | ||||||
|             return $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')}{(IsExtrapolated ? 'E' : '_')} {Beat:N4}/{LoopBeats})"; |             return $"{nameof(BeatTimestamp)}({(IsLooping ? 'Y' : 'n')} {Beat:N4}/{LoopBeats})"; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -908,16 +872,13 @@ namespace MuzikaGromche | ||||||
|         public readonly float BeatFromExclusive; |         public readonly float BeatFromExclusive; | ||||||
|         // Closed upper bound |         // Closed upper bound | ||||||
|         public readonly float BeatToInclusive; |         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, bool isExtrapolated) |         public BeatTimeSpan(int loopBeats, bool isLooping, float beatFromExclusive, float beatToInclusive) | ||||||
|         { |         { | ||||||
|             LoopBeats = loopBeats; |             LoopBeats = loopBeats; | ||||||
|             IsLooping = isLooping || beatToInclusive >= HalfLoopBeats; |             IsLooping = isLooping || beatToInclusive >= HalfLoopBeats; | ||||||
|             BeatFromExclusive = wrap(beatFromExclusive); |             BeatFromExclusive = wrap(beatFromExclusive); | ||||||
|             BeatToInclusive = wrap(beatToInclusive); |             BeatToInclusive = wrap(beatToInclusive); | ||||||
|             IsExtrapolated = isExtrapolated; |  | ||||||
| 
 | 
 | ||||||
|             float wrap(float beat) |             float wrap(float beat) | ||||||
|             { |             { | ||||||
|  | @ -927,20 +888,20 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|         public static BeatTimeSpan Between(BeatTimestamp timestampFromExclusive, BeatTimestamp timestampToInclusive) |         public static BeatTimeSpan Between(BeatTimestamp timestampFromExclusive, BeatTimestamp timestampToInclusive) | ||||||
|         { |         { | ||||||
|             var isExtrapolated = timestampFromExclusive.IsExtrapolated || timestampToInclusive.IsExtrapolated; |             return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat); | ||||||
|             return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat, isExtrapolated); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|         public static BeatTimeSpan Between(float beatFromExclusive, BeatTimestamp timestampToInclusive) |         public static BeatTimeSpan Between(float beatFromExclusive, BeatTimestamp timestampToInclusive) | ||||||
|         { |         { | ||||||
|             return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, beatFromExclusive, timestampToInclusive.Beat, timestampToInclusive.IsExtrapolated); |             return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, beatFromExclusive, timestampToInclusive.Beat); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public static BeatTimeSpan Empty = new(); |         public static BeatTimeSpan Empty = new(); | ||||||
| 
 | 
 | ||||||
|         public readonly BeatTimestamp ToTimestamp() |         public readonly BeatTimestamp ToTimestamp() | ||||||
|         { |         { | ||||||
|             return new(LoopBeats, IsLooping, BeatToInclusive, IsExtrapolated); |             return new(LoopBeats, IsLooping, BeatToInclusive); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // The beat will not be wrapped. |         // The beat will not be wrapped. | ||||||
|  | @ -962,7 +923,7 @@ namespace MuzikaGromche | ||||||
|                 // before wrapping (happens earlier) and after wrapping (happens later). |                 // before wrapping (happens earlier) and after wrapping (happens later). | ||||||
| 
 | 
 | ||||||
|                 // Check the "happens later" part first. |                 // Check the "happens later" part first. | ||||||
|                 var laterSpan = new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: /* epsilon to make zero inclusive */ -0.001f, beatToInclusive: BeatToInclusive, IsExtrapolated); |                 var laterSpan = new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: /* epsilon to make zero inclusive */ -0.001f, beatToInclusive: BeatToInclusive); | ||||||
|                 var laterIndex = laterSpan.GetLastIndex(timeSeries); |                 var laterIndex = laterSpan.GetLastIndex(timeSeries); | ||||||
|                 if (laterIndex != null) |                 if (laterIndex != null) | ||||||
|                 { |                 { | ||||||
|  | @ -1048,144 +1009,94 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|         public readonly override string ToString() |         public readonly override string ToString() | ||||||
|         { |         { | ||||||
|             return $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')}{(IsExtrapolated ? 'E' : '_')}, {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty!" : "")})"; |             return $"{nameof(BeatTimeSpan)}({(IsLooping ? 'Y' : 'n')}, {BeatFromExclusive:N4}..{BeatToInclusive:N4}/{LoopBeats}{(IsEmpty() ? " Empty!" : "")})"; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     class ExtrapolatedAudioSourceState |     class BeatTimeState | ||||||
|     { |     { | ||||||
|         // AudioSource.isPlaying |         // The object is newly created, the Start audio began to play but its time hasn't adjanced from 0.0f yet. | ||||||
|         public bool IsPlaying { get; private set; } |         private bool hasStarted = false; | ||||||
| 
 | 
 | ||||||
|         // AudioSource.time, possibly extrapolated |         // The time span after Zero state (when the Start audio has advanced from 0.0f) but before the WindUpTimer+Loop/2. | ||||||
|         public float Time => ExtrapolatedTime; |         private bool windUpOffsetIsLooping = false; | ||||||
| 
 | 
 | ||||||
|         // The object is newly created, the AudioSource began to play (possibly delayed) but its time hasn't advanced from 0.0f yet. |         // The time span after Zero state (when the Start audio has advanced from 0.0f) but before the WindUpTimer+LoopOffset+Loop/2. | ||||||
|         // Time can not be extrapolated when HasStarted is false. |         private bool loopOffsetIsLooping = false; | ||||||
|         public bool HasStarted { get; private set; } = false; |  | ||||||
| 
 | 
 | ||||||
|         public bool IsExtrapolated => LastKnownNonExtrapolatedTime != ExtrapolatedTime; |         private bool windUpZeroBeatEventTriggered = false; | ||||||
| 
 | 
 | ||||||
|         private float ExtrapolatedTime = 0f; |         private readonly Track track; | ||||||
| 
 | 
 | ||||||
|         private float LastKnownNonExtrapolatedTime = 0f; |         private float loopOffsetBeat = float.NegativeInfinity; | ||||||
| 
 | 
 | ||||||
|         // Any wall clock based measurements of when this state was recorded |         private static System.Random lyricsRandom = null!; | ||||||
|         private float LastKnownRealtime = 0f; |  | ||||||
| 
 | 
 | ||||||
|         private const float MaxExtrapolationInterval = 0.5f; |         private int lyricsRandomPerLoop; | ||||||
| 
 | 
 | ||||||
|         public void Update(AudioSource audioSource, float realtime) |         public BeatTimeState(Track track) | ||||||
|         { |         { | ||||||
|             IsPlaying = audioSource.isPlaying; |             if (lyricsRandom == null) | ||||||
|             HasStarted |= audioSource.time != 0f; |  | ||||||
| 
 |  | ||||||
|             if (LastKnownNonExtrapolatedTime != audioSource.time) |  | ||||||
|             { |             { | ||||||
|                 LastKnownRealtime = realtime; |                 lyricsRandom = new System.Random(RoundManager.Instance.playersManager.randomMapSeed + 1337); | ||||||
|                 LastKnownNonExtrapolatedTime = ExtrapolatedTime = audioSource.time; |                 lyricsRandomPerLoop = lyricsRandom.Next(); | ||||||
|             } |             } | ||||||
|             // Frames are rendering faster than AudioSource updates its playback time state |             this.track = track; | ||||||
|             else if (IsPlaying && HasStarted && Config.ExtrapolateTime) |         } | ||||||
|  | 
 | ||||||
|  |         public List<BaseEvent> Update(AudioSource start, AudioSource loop) | ||||||
|         { |         { | ||||||
|  |             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 | #if DEBUG | ||||||
|                 Debug.Assert(LastKnownNonExtrapolatedTime == audioSource.time); // implied |                     Debug.Log($"{nameof(MuzikaGromche)} looping? {(loopOffsetIsLooping ? 'X' : '_')}{(windUpOffsetIsLooping ? 'X' : '_')} Loop={loopOffsetSpan} WindUp={windUpOffsetTimestamp} events={string.Join(",", events)}"); | ||||||
| #endif | #endif | ||||||
|                 var deltaTime = realtime - LastKnownRealtime; |                     return events; | ||||||
|                 if (0 < deltaTime && deltaTime < MaxExtrapolationInterval) |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Events other than colors start rotating at 0=WindUpTimer+LoopOffset. | ||||||
|  |         private BeatTimestamp UpdateStateForLoopOffset(AudioSource start, AudioSource loop) | ||||||
|         { |         { | ||||||
|                     ExtrapolatedTime = LastKnownNonExtrapolatedTime + deltaTime; |             var offset = BaseOffset() + track.LoopOffsetInSeconds; | ||||||
|                 } |             var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, loopOffsetIsLooping); | ||||||
|             } |             loopOffsetIsLooping |= timestamp.IsLooping; | ||||||
|  |             return timestamp; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public void Finish() |         // Colors start rotating at 0=WindUpTimer | ||||||
|  |         private BeatTimestamp UpdateStateForWindUpOffset(AudioSource start, AudioSource loop) | ||||||
|         { |         { | ||||||
|             IsPlaying = false; |             var offset = BaseOffset(); | ||||||
|  |             var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, windUpOffsetIsLooping); | ||||||
|  |             windUpOffsetIsLooping |= timestamp.IsLooping; | ||||||
|  |             return timestamp; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         public override string ToString() |         private float BaseOffset() | ||||||
|         { |         { | ||||||
|             return $"{nameof(ExtrapolatedAudioSourceState)}({(IsPlaying ? 'P' : '_')}{(HasStarted ? 'S' : '0')} " |             return Config.AudioOffset.Value + track.BeatsOffsetInSeconds + track.WindUpTimer; | ||||||
|                 + (IsExtrapolated |  | ||||||
|                     ? $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4} => {ExtrapolatedTime:N4}" |  | ||||||
|                     : $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4}" |  | ||||||
|                 ) + ")"; |  | ||||||
|         } |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     class JesterAudioSourcesState |         BeatTimestamp GetTimestampRelativeToGivenOffset(AudioSource start, AudioSource loop, float offset, bool isLooping) | ||||||
|     { |  | ||||||
|         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. |             // If popped, calculate which beat the music is currently at. | ||||||
|             // In order to do that we should choose one of two strategies: |             // In order to do that we should choose one of two strategies: | ||||||
|  | @ -1198,114 +1109,42 @@ namespace MuzikaGromche | ||||||
|             // NOTE 2: There is a weird state when Jester has popped and chases a player: |             // 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. |             //         Start/farAudio isPlaying is true but stays exactly at zero time, so we need to ignore that. | ||||||
| 
 | 
 | ||||||
|             var offset = StartOfLoop + additionalOffset; |             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; | ||||||
| 
 | 
 | ||||||
|             float timeSinceStartOfLoop = time - offset; |             float adjustedTimeFromOffset = timeFromTheVeryStart - offset; | ||||||
| 
 | 
 | ||||||
|             var adjustedTimeNormalized = timeSinceStartOfLoop / LoopLength; |             var adjustedTimeNormalized = adjustedTimeFromOffset / track.LoadedLoop.length; | ||||||
| 
 | 
 | ||||||
|             var beat = adjustedTimeNormalized * Beats; |             var beat = adjustedTimeNormalized * track.Beats; | ||||||
| 
 | 
 | ||||||
|             // Let it infer the isLooping flag from the beat |             // Let it infer the isLooping flag from the beat | ||||||
|             var timestamp = new BeatTimestamp(Beats, IsLooping, beat, isExtrapolated); |             var timestamp = new BeatTimestamp(track.Beats, isLooping, beat); | ||||||
| 
 |  | ||||||
|             IsLooping |= timestamp.IsLooping; |  | ||||||
| 
 | 
 | ||||||
| #if DEBUG && false | #if DEBUG && false | ||||||
|             Debug.LogFormat("{0} t={1,10:N4} d={2,7:N4} {3} Time={4:N4} norm={5,6:N4} beat={6,7:N4}", |             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}", | ||||||
|                 nameof(MuzikaGromche), |                 nameof(MuzikaGromche), | ||||||
|                 Time.realtimeSinceStartup, Time.deltaTime, |                 Time.realtimeSinceStartup, Time.deltaTime, | ||||||
|                 isExtrapolated ? 'E' : '_', time, |                 (start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'), | ||||||
|                 adjustedTimeNormalized, beat); |                 (loop.isPlaying ? '+' : ' '), loop.time, | ||||||
|  |                 adjustedTimeNormalized, beat, color); | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|             return timestamp; |             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) |         private List<BaseEvent> GetEvents(BeatTimeSpan loopOffsetSpan, BeatTimestamp windUpOffsetTimestamp) | ||||||
|         { |         { | ||||||
|             List<BaseEvent> events = []; |             List<BaseEvent> events = []; | ||||||
| 
 | 
 | ||||||
|             if (windUpOffsetTimestamp.Beat >= 0f && !WindUpZeroBeatEventTriggered) |             if (windUpOffsetTimestamp.Beat >= 0f && !windUpZeroBeatEventTriggered) | ||||||
|             { |             { | ||||||
|                 events.Add(new WindUpZeroBeatEvent()); |                 events.Add(new WindUpZeroBeatEvent()); | ||||||
|                 WindUpZeroBeatEventTriggered = true; |                 windUpZeroBeatEventTriggered = true; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (GetColorEvent(loopOffsetSpan, windUpOffsetTimestamp) is { } colorEvent) |             if (GetColorEvent(loopOffsetSpan, windUpOffsetTimestamp) is { } colorEvent) | ||||||
|  | @ -1327,7 +1166,7 @@ namespace MuzikaGromche | ||||||
|                 { |                 { | ||||||
|                     var line = track.LyricsLines[i]; |                     var line = track.LyricsLines[i]; | ||||||
|                     var alternatives = line.Split('\t'); |                     var alternatives = line.Split('\t'); | ||||||
|                     var randomIndex = LyricsRandomPerLoop % alternatives.Length; |                     var randomIndex = lyricsRandomPerLoop % alternatives.Length; | ||||||
|                     var alternative = alternatives[randomIndex]; |                     var alternative = alternatives[randomIndex]; | ||||||
|                     if (alternative != "") |                     if (alternative != "") | ||||||
|                     { |                     { | ||||||
|  | @ -1594,7 +1433,6 @@ namespace MuzikaGromche | ||||||
|         readonly public int TotalWeights { get; } |         readonly public int TotalWeights { get; } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| #if DEBUG |  | ||||||
|     static class SyncedEntryExtensions |     static class SyncedEntryExtensions | ||||||
|     { |     { | ||||||
|         // Update local values on clients. Even though the clients couldn't |         // Update local values on clients. Even though the clients couldn't | ||||||
|  | @ -1607,12 +1445,8 @@ namespace MuzikaGromche | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| #endif |  | ||||||
| 
 | 
 | ||||||
|     class Config |     class Config : SyncedConfig2<Config> | ||||||
| #if DEBUG |  | ||||||
|         : SyncedConfig2<Config> |  | ||||||
| #endif |  | ||||||
|     { |     { | ||||||
|         public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!; |         public static ConfigEntry<bool> DisplayLyrics { get; private set; } = null!; | ||||||
| 
 | 
 | ||||||
|  | @ -1622,7 +1456,6 @@ namespace MuzikaGromche | ||||||
| 
 | 
 | ||||||
|         public static ConfigEntry<bool> OverrideSpawnRates { get; private set; } = null!; |         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 bool ShouldSkipWindingPhase { get; private set; } = false; | ||||||
| 
 | 
 | ||||||
|         public static Palette? PaletteOverride { get; private set; } = null; |         public static Palette? PaletteOverride { get; private set; } = null; | ||||||
|  | @ -1636,10 +1469,7 @@ namespace MuzikaGromche | ||||||
|         public static float? ColorTransitionOutOverride { get; private set; } = null; |         public static float? ColorTransitionOutOverride { get; private set; } = null; | ||||||
|         public static string? ColorTransitionEasingOverride { get; private set; } = null; |         public static string? ColorTransitionEasingOverride { get; private set; } = null; | ||||||
| 
 | 
 | ||||||
|         internal Config(ConfigFile configFile) |         internal Config(ConfigFile configFile) : base(PluginInfo.PLUGIN_GUID) | ||||||
| #if DEBUG |  | ||||||
|             : base(PluginInfo.PLUGIN_GUID) |  | ||||||
| #endif |  | ||||||
|         { |         { | ||||||
|             DisplayLyrics = configFile.Bind("General", "Display Lyrics", true, |             DisplayLyrics = configFile.Bind("General", "Display Lyrics", true, | ||||||
|                 new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music.")); |                 new ConfigDescription("Display lyrics in the HUD tooltip when you hear the music.")); | ||||||
|  | @ -1659,7 +1489,6 @@ namespace MuzikaGromche | ||||||
|             LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions()))); |             LethalConfigManager.AddConfigItem(new BoolCheckBoxConfigItem(OverrideSpawnRates, Default(new BoolCheckBoxOptions()))); | ||||||
| 
 | 
 | ||||||
| #if DEBUG | #if DEBUG | ||||||
|             SetupEntriesForExtrapolation(configFile); |  | ||||||
|             SetupEntriesToSkipWinding(configFile); |             SetupEntriesToSkipWinding(configFile); | ||||||
|             SetupEntriesForPaletteOverride(configFile); |             SetupEntriesForPaletteOverride(configFile); | ||||||
|             SetupEntriesForTimingsOverride(configFile); |             SetupEntriesForTimingsOverride(configFile); | ||||||
|  | @ -1708,12 +1537,9 @@ namespace MuzikaGromche | ||||||
|                 LethalConfigManager.AddConfigItem(new IntSliderConfigItem(track.Weight, Default(new IntSliderOptions()))); |                 LethalConfigManager.AddConfigItem(new IntSliderConfigItem(track.Weight, Default(new IntSliderOptions()))); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| #if DEBUG |  | ||||||
|             ConfigManager.Register(this); |             ConfigManager.Register(this); | ||||||
| #endif |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| #if DEBUG |  | ||||||
|         // HACK because CSync doesn't provide an API to register a list of config entries |         // HACK because CSync doesn't provide an API to register a list of config entries | ||||||
|         // See https://github.com/lc-sigurd/CSync/issues/11 |         // See https://github.com/lc-sigurd/CSync/issues/11 | ||||||
|         private void CSyncHackAddSyncedEntry(SyncedEntryBase entryBase) |         private void CSyncHackAddSyncedEntry(SyncedEntryBase entryBase) | ||||||
|  | @ -1721,7 +1547,6 @@ namespace MuzikaGromche | ||||||
|             // This is basically what ConfigFile.PopulateEntryContainer does |             // This is basically what ConfigFile.PopulateEntryContainer does | ||||||
|             EntryContainer.Add(entryBase.BoxedEntry.ToSyncedEntryIdentifier(), entryBase); |             EntryContainer.Add(entryBase.BoxedEntry.ToSyncedEntryIdentifier(), entryBase); | ||||||
|         } |         } | ||||||
| #endif |  | ||||||
| 
 | 
 | ||||||
|         public static CanModifyResult CanModifyIfHost() |         public static CanModifyResult CanModifyIfHost() | ||||||
|         { |         { | ||||||
|  | @ -1757,23 +1582,6 @@ namespace MuzikaGromche | ||||||
|             return CanModifyResult.True(); |             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) |         private void SetupEntriesToSkipWinding(ConfigFile configFile) | ||||||
|         { |         { | ||||||
|             var syncedEntry = configFile.BindSyncedEntry("General", "Skip Winding Phase", false, |             var syncedEntry = configFile.BindSyncedEntry("General", "Skip Winding Phase", false, | ||||||
|  | @ -1885,7 +1693,7 @@ namespace MuzikaGromche | ||||||
|             fadeOutBeatSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Beat", 0f, |             fadeOutBeatSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Beat", 0f, | ||||||
|                 new ConfigDescription("The beat at which to start fading out", new AcceptableValueRange<float>(-1000f, 0))); |                 new ConfigDescription("The beat at which to start fading out", new AcceptableValueRange<float>(-1000f, 0))); | ||||||
|             fadeOutDurationSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Duration", 0f, |             fadeOutDurationSyncedEntry = configFile.BindSyncedEntry(section, "Fade Out Duration", 0f, | ||||||
|                 new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 10))); |                 new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 100))); | ||||||
|             flickerLightsTimeSeriesSyncedEntry = configFile.BindSyncedEntry(section, "Flicker Lights Time Series", "", |             flickerLightsTimeSeriesSyncedEntry = configFile.BindSyncedEntry(section, "Flicker Lights Time Series", "", | ||||||
|                 new ConfigDescription("Time series of beat offsets when to flicker the lights.")); |                 new ConfigDescription("Time series of beat offsets when to flicker the lights.")); | ||||||
|             lyricsTimeSeriesSyncedEntry = configFile.BindSyncedEntry(section, "Lyrics Time Series", "", |             lyricsTimeSeriesSyncedEntry = configFile.BindSyncedEntry(section, "Lyrics Time Series", "", | ||||||
|  | @ -1981,7 +1789,6 @@ namespace MuzikaGromche | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| #endif |  | ||||||
| 
 | 
 | ||||||
|         private T Default<T>(T options) where T : BaseOptions |         private T Default<T>(T options) where T : BaseOptions | ||||||
|         { |         { | ||||||
|  | @ -2021,30 +1828,15 @@ namespace MuzikaGromche | ||||||
|             ChooseTrackDeferred(); |             ChooseTrackDeferred(); | ||||||
|             foreach (var track in Plugin.Tracks) |             foreach (var track in Plugin.Tracks) | ||||||
|             { |             { | ||||||
|                 track.Weight.SettingChanged += ChooseTrackDeferredDelegate; |                 track.Weight.SettingChanged += (_, _) => ChooseTrackDeferred(); | ||||||
|             } |             } | ||||||
|             Config.SkipExplicitTracks.SettingChanged += ChooseTrackDeferredDelegate; |             Config.SkipExplicitTracks.SettingChanged += (_, _) => ChooseTrackDeferred(); | ||||||
|             base.OnNetworkSpawn(); |             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 |         // Batch multiple weights changes in a single network RPC | ||||||
|         private Coroutine? DeferredCoroutine = null; |         private Coroutine? DeferredCoroutine = null; | ||||||
| 
 | 
 | ||||||
|         private void ChooseTrackDeferredDelegate(object sender, EventArgs e) |  | ||||||
|         { |  | ||||||
|             ChooseTrackDeferred(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         private void ChooseTrackDeferred() |         private void ChooseTrackDeferred() | ||||||
|         { |         { | ||||||
|             if (DeferredCoroutine != null) |             if (DeferredCoroutine != null) | ||||||
|  | @ -2176,7 +1968,7 @@ namespace MuzikaGromche | ||||||
|                 __instance.farAudio = __state.farAudio; |                 __instance.farAudio = __state.farAudio; | ||||||
| 
 | 
 | ||||||
|                 var time = __instance.farAudio.time; |                 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 |                 // Override screamingSFX with Loop, delayed by the remaining time of the Start audio | ||||||
|                 __instance.creatureVoice.Stop(); |                 __instance.creatureVoice.Stop(); | ||||||
|  | @ -2189,9 +1981,9 @@ namespace MuzikaGromche | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // Manage the timeline: switch color of the lights according to the current playback/beat position. |             // Manage the timeline: switch color of the lights according to the current playback/beat position. | ||||||
|             if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState is { } beatTimeState) |             if ((__instance.previousState == 1 || __instance.previousState == 2) && Plugin.BeatTimeState != null) | ||||||
|             { |             { | ||||||
|                 var events = beatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice); |                 var events = Plugin.BeatTimeState.Update(start: __instance.farAudio, loop: __instance.creatureVoice); | ||||||
|                 foreach (var ev in events) |                 foreach (var ev in events) | ||||||
|                 { |                 { | ||||||
|                     switch (ev) |                     switch (ev) | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ To keep it a surprise, it is adviced that you do not read the detailed descripti | ||||||
| 
 | 
 | ||||||
| Muzika Gromche is compatible with *Almost Vanilla™* gameplay and [*High Quota Mindset*](https://youtu.be/18RUCgQldGg?t=2553). It slightly changes certain timers, so won't be compatible with leaderboards. If you are a streamer™, be aware that it does play *copyrighted content.* | Muzika Gromche is compatible with *Almost Vanilla™* gameplay and [*High Quota Mindset*](https://youtu.be/18RUCgQldGg?t=2553). It slightly changes certain timers, so won't be compatible with leaderboards. If you are a streamer™, be aware that it does play *copyrighted content.* | ||||||
| 
 | 
 | ||||||
| Muzika Gromche works with all Lethal Company versions from v72 all the way back to v40, and is likely to work on all future versions as long as dependencies ([`LethalConfig`] and [`LobbyCompatibility`]) are working. | Muzika Gromche works with all Lethal Company versions from v72 all the way back to v40, and is likely to work on all future versions as long as dependencies ([`CSync`] and [`LethalConfig`]) are working. | ||||||
| 
 | 
 | ||||||
| Speaking of dependencies, [`V70PoweredLights_Fix`] is not strictly required, but it doesn't hurt to have it installed on any version, and it makes this mod more enjoyable on new Mansion tiles. | Speaking of dependencies, [`V70PoweredLights_Fix`] is not strictly required, but it doesn't hurt to have it installed on any version, and it makes this mod more enjoyable on new Mansion tiles. | ||||||
| 
 | 
 | ||||||
|  | @ -38,5 +38,4 @@ Any player can change their personal preferences locally. | ||||||
| 
 | 
 | ||||||
| [`CSync`]: https://thunderstore.io/c/lethal-company/p/Sigurd/CSync/ | [`CSync`]: https://thunderstore.io/c/lethal-company/p/Sigurd/CSync/ | ||||||
| [`LethalConfig`]: https://thunderstore.io/c/lethal-company/p/AinaVT/LethalConfig/ | [`LethalConfig`]: https://thunderstore.io/c/lethal-company/p/AinaVT/LethalConfig/ | ||||||
| [`LobbyCompatibility`]: https://thunderstore.io/c/lethal-company/p/BMX/LobbyCompatibility/ |  | ||||||
| [`V70PoweredLights_Fix`]: https://thunderstore.io/c/lethal-company/p/WaterGun/V70PoweredLights_Fix/ | [`V70PoweredLights_Fix`]: https://thunderstore.io/c/lethal-company/p/WaterGun/V70PoweredLights_Fix/ | ||||||
|  |  | ||||||
|  | @ -1,11 +1,12 @@ | ||||||
| { | { | ||||||
|     "name": "MuzikaGromche", |     "name": "MuzikaGromche", | ||||||
|     "version_number": "1337.420.69", |     "version_number": "1337.69.420", | ||||||
|     "author": "Ratijas", |     "author": "Ratijas", | ||||||
|     "description": "Add some content to your inverse teleporter experience on Titan!", |     "description": "Add some content to your inverse teleporter experience on Titan!", | ||||||
|     "website_url": "https://git.vilunov.me/ratijas/muzika-gromche", |     "website_url": "https://git.vilunov.me/ratijas/muzika-gromche", | ||||||
|     "dependencies": [ |     "dependencies": [ | ||||||
|         "BepInEx-BepInExPack-5.4.2100", |         "BepInEx-BepInExPack-5.4.2100", | ||||||
|  |         "Sigurd-CSync-5.0.1", | ||||||
|         "AinaVT-LethalConfig-1.4.6", |         "AinaVT-LethalConfig-1.4.6", | ||||||
|         "WaterGun-V70PoweredLights_Fix-1.0.0", |         "WaterGun-V70PoweredLights_Fix-1.0.0", | ||||||
|         "BMX-LobbyCompatibility-1.5.1" |         "BMX-LobbyCompatibility-1.5.1" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue