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 | ||||
| 
 | ||||
| ## 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 | ||||
| 
 | ||||
| - Fix certain object hanging around after being disabled. | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|         <AssemblyName>Ratijas.MuzikaGromche</AssemblyName> | ||||
|         <Product>Muzika Gromche</Product> | ||||
|         <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> | ||||
|         <LangVersion>latest</LangVersion> | ||||
|         <Nullable>enable</Nullable> | ||||
|  | @ -41,7 +41,6 @@ | |||
|         <!-- | ||||
|             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 | ||||
|             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="AinaVT-LethalConfig" Version="1.4.6" PrivateAssets="all" Private="false" /> | ||||
|  |  | |||
|  | @ -1,5 +1,7 @@ | |||
| using BepInEx; | ||||
| using BepInEx.Configuration; | ||||
| using CSync.Extensions; | ||||
| using CSync.Lib; | ||||
| using HarmonyLib; | ||||
| using LethalConfig; | ||||
| using LethalConfig.ConfigItems; | ||||
|  | @ -19,17 +21,10 @@ 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)] | ||||
|  | @ -475,27 +470,6 @@ 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() | ||||
|  | @ -555,9 +529,6 @@ 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++) | ||||
|  | @ -578,9 +549,6 @@ 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(); | ||||
|  | @ -857,15 +825,11 @@ namespace MuzikaGromche | |||
|         // Beat relative to the popup. Always less than LoopBeats. When not IsLooping, can be unbounded negative. | ||||
|         public readonly 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) | ||||
|         public BeatTimestamp(int loopBeats, bool isLooping, float beat) | ||||
|         { | ||||
|             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) | ||||
|  | @ -878,7 +842,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, self.IsExtrapolated); | ||||
|             return new BeatTimestamp(self.LoopBeats, self.IsLooping, self.Beat + 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 | ||||
|             var beat = Mathf.Floor(Beat); | ||||
|             return new BeatTimestamp(LoopBeats, IsLooping, beat, IsExtrapolated); | ||||
|             return new BeatTimestamp(LoopBeats, IsLooping, beat); | ||||
|         } | ||||
| 
 | ||||
|         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; | ||||
|         // 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, bool isExtrapolated) | ||||
|         public BeatTimeSpan(int loopBeats, bool isLooping, float beatFromExclusive, float beatToInclusive) | ||||
|         { | ||||
|             LoopBeats = loopBeats; | ||||
|             IsLooping = isLooping || beatToInclusive >= HalfLoopBeats; | ||||
|             BeatFromExclusive = wrap(beatFromExclusive); | ||||
|             BeatToInclusive = wrap(beatToInclusive); | ||||
|             IsExtrapolated = isExtrapolated; | ||||
| 
 | ||||
|             float wrap(float beat) | ||||
|             { | ||||
|  | @ -927,20 +888,20 @@ namespace MuzikaGromche | |||
| 
 | ||||
|         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, isExtrapolated); | ||||
|             return new BeatTimeSpan(timestampToInclusive.LoopBeats, timestampToInclusive.IsLooping, timestampFromExclusive.Beat, timestampToInclusive.Beat); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         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 readonly BeatTimestamp ToTimestamp() | ||||
|         { | ||||
|             return new(LoopBeats, IsLooping, BeatToInclusive, IsExtrapolated); | ||||
|             return new(LoopBeats, IsLooping, BeatToInclusive); | ||||
|         } | ||||
| 
 | ||||
|         // The beat will not be wrapped. | ||||
|  | @ -962,7 +923,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, IsExtrapolated); | ||||
|                 var laterSpan = new BeatTimeSpan(LoopBeats, isLooping: false, beatFromExclusive: /* epsilon to make zero inclusive */ -0.001f, beatToInclusive: BeatToInclusive); | ||||
|                 var laterIndex = laterSpan.GetLastIndex(timeSeries); | ||||
|                 if (laterIndex != null) | ||||
|                 { | ||||
|  | @ -1048,144 +1009,94 @@ namespace MuzikaGromche | |||
| 
 | ||||
|         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 | ||||
|         public bool IsPlaying { get; private set; } | ||||
|         // The object is newly created, the Start audio began to play but its time hasn't adjanced from 0.0f yet. | ||||
|         private bool hasStarted = false; | ||||
| 
 | ||||
|         // AudioSource.time, possibly extrapolated | ||||
|         public float Time => ExtrapolatedTime; | ||||
|         // The time span after Zero state (when the Start audio has advanced from 0.0f) but before the WindUpTimer+Loop/2. | ||||
|         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. | ||||
|         // Time can not be extrapolated when HasStarted is false. | ||||
|         public bool HasStarted { get; private set; } = false; | ||||
|         // 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; | ||||
| 
 | ||||
|         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 float LastKnownRealtime = 0f; | ||||
|         private static System.Random lyricsRandom = null!; | ||||
| 
 | ||||
|         private const float MaxExtrapolationInterval = 0.5f; | ||||
|         private int lyricsRandomPerLoop; | ||||
| 
 | ||||
|         public void Update(AudioSource audioSource, float realtime) | ||||
|         public BeatTimeState(Track track) | ||||
|         { | ||||
|             IsPlaying = audioSource.isPlaying; | ||||
|             HasStarted |= audioSource.time != 0f; | ||||
| 
 | ||||
|             if (LastKnownNonExtrapolatedTime != audioSource.time) | ||||
|             if (lyricsRandom == null) | ||||
|             { | ||||
|                 LastKnownRealtime = realtime; | ||||
|                 LastKnownNonExtrapolatedTime = ExtrapolatedTime = audioSource.time; | ||||
|                 lyricsRandom = new System.Random(RoundManager.Instance.playersManager.randomMapSeed + 1337); | ||||
|                 lyricsRandomPerLoop = lyricsRandom.Next(); | ||||
|             } | ||||
|             // Frames are rendering faster than AudioSource updates its playback time state | ||||
|             else if (IsPlaying && HasStarted && Config.ExtrapolateTime) | ||||
|             this.track = track; | ||||
|         } | ||||
| 
 | ||||
|         public List<BaseEvent> Update(AudioSource start, AudioSource loop) | ||||
|         { | ||||
|             hasStarted |= start.time != 0; | ||||
|             if (hasStarted) | ||||
|             { | ||||
| #if DEBUG | ||||
|                 Debug.Assert(LastKnownNonExtrapolatedTime == audioSource.time); // implied | ||||
| #endif | ||||
|                 var deltaTime = realtime - LastKnownRealtime; | ||||
|                 if (0 < deltaTime && deltaTime < MaxExtrapolationInterval) | ||||
|                 var loopTimestamp = UpdateStateForLoopOffset(start, loop); | ||||
|                 var loopOffsetSpan = BeatTimeSpan.Between(loopOffsetBeat, loopTimestamp); | ||||
| 
 | ||||
|                 // Do not go back in time | ||||
|                 if (!loopOffsetSpan.IsEmpty()) | ||||
|                 { | ||||
|                     ExtrapolatedTime = LastKnownNonExtrapolatedTime + deltaTime; | ||||
|                     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)}"); | ||||
| #endif | ||||
|                     return events; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         public void Finish() | ||||
|         // Events other than colors start rotating at 0=WindUpTimer+LoopOffset. | ||||
|         private BeatTimestamp UpdateStateForLoopOffset(AudioSource start, AudioSource loop) | ||||
|         { | ||||
|             IsPlaying = false; | ||||
|             var offset = BaseOffset() + track.LoopOffsetInSeconds; | ||||
|             var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, loopOffsetIsLooping); | ||||
|             loopOffsetIsLooping |= timestamp.IsLooping; | ||||
|             return timestamp; | ||||
|         } | ||||
| 
 | ||||
|         public override string ToString() | ||||
|         // Colors start rotating at 0=WindUpTimer | ||||
|         private BeatTimestamp UpdateStateForWindUpOffset(AudioSource start, AudioSource loop) | ||||
|         { | ||||
|             return $"{nameof(ExtrapolatedAudioSourceState)}({(IsPlaying ? 'P' : '_')}{(HasStarted ? 'S' : '0')} " | ||||
|                 + (IsExtrapolated | ||||
|                     ? $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4} => {ExtrapolatedTime:N4}" | ||||
|                     : $"{LastKnownRealtime:N4}, {LastKnownNonExtrapolatedTime:N4}" | ||||
|                 ) + ")"; | ||||
|             var offset = BaseOffset(); | ||||
|             var timestamp = GetTimestampRelativeToGivenOffset(start, loop, offset, windUpOffsetIsLooping); | ||||
|             windUpOffsetIsLooping |= timestamp.IsLooping; | ||||
|             return timestamp; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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) | ||||
|         private float BaseOffset() | ||||
|         { | ||||
|             StartClipLength = startClipLength; | ||||
|             return Config.AudioOffset.Value + track.BeatsOffsetInSeconds + track.WindUpTimer; | ||||
|         } | ||||
| 
 | ||||
|         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) | ||||
|         BeatTimestamp GetTimestampRelativeToGivenOffset(AudioSource start, AudioSource loop, float offset, bool isLooping) | ||||
|         { | ||||
|             // If popped, calculate which beat the music is currently at. | ||||
|             // 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: | ||||
|             //         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 | ||||
|             var timestamp = new BeatTimestamp(Beats, IsLooping, beat, isExtrapolated); | ||||
| 
 | ||||
|             IsLooping |= timestamp.IsLooping; | ||||
|             var timestamp = new BeatTimestamp(track.Beats, isLooping, beat); | ||||
| 
 | ||||
| #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), | ||||
|                 Time.realtimeSinceStartup, Time.deltaTime, | ||||
|                 isExtrapolated ? 'E' : '_', time, | ||||
|                 adjustedTimeNormalized, beat); | ||||
|                 (start.isPlaying ? '+' : ' '), start.time, (start.time == 0f ? 'Y' : 'n'), | ||||
|                 (loop.isPlaying ? '+' : ' '), loop.time, | ||||
|                 adjustedTimeNormalized, beat, color); | ||||
| #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 && !windUpZeroBeatEventTriggered) | ||||
|             { | ||||
|                 events.Add(new WindUpZeroBeatEvent()); | ||||
|                 WindUpZeroBeatEventTriggered = true; | ||||
|                 windUpZeroBeatEventTriggered = true; | ||||
|             } | ||||
| 
 | ||||
|             if (GetColorEvent(loopOffsetSpan, windUpOffsetTimestamp) is { } colorEvent) | ||||
|  | @ -1327,7 +1166,7 @@ namespace MuzikaGromche | |||
|                 { | ||||
|                     var line = track.LyricsLines[i]; | ||||
|                     var alternatives = line.Split('\t'); | ||||
|                     var randomIndex = LyricsRandomPerLoop % alternatives.Length; | ||||
|                     var randomIndex = lyricsRandomPerLoop % alternatives.Length; | ||||
|                     var alternative = alternatives[randomIndex]; | ||||
|                     if (alternative != "") | ||||
|                     { | ||||
|  | @ -1594,7 +1433,6 @@ namespace MuzikaGromche | |||
|         readonly public int TotalWeights { get; } | ||||
|     } | ||||
| 
 | ||||
| #if DEBUG | ||||
|     static class SyncedEntryExtensions | ||||
|     { | ||||
|         // Update local values on clients. Even though the clients couldn't | ||||
|  | @ -1607,12 +1445,8 @@ namespace MuzikaGromche | |||
|             }; | ||||
|         } | ||||
|     } | ||||
| #endif | ||||
| 
 | ||||
|     class Config | ||||
| #if DEBUG | ||||
|         : SyncedConfig2<Config> | ||||
| #endif | ||||
|     class Config : SyncedConfig2<Config> | ||||
|     { | ||||
|         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 bool ExtrapolateTime { get; private set; } = true; | ||||
|         public static bool ShouldSkipWindingPhase { get; private set; } = false; | ||||
| 
 | ||||
|         public static Palette? PaletteOverride { get; private set; } = null; | ||||
|  | @ -1636,10 +1469,7 @@ namespace MuzikaGromche | |||
|         public static float? ColorTransitionOutOverride { get; private set; } = null; | ||||
|         public static string? ColorTransitionEasingOverride { get; private set; } = null; | ||||
| 
 | ||||
|         internal Config(ConfigFile configFile) | ||||
| #if DEBUG | ||||
|             : base(PluginInfo.PLUGIN_GUID) | ||||
| #endif | ||||
|         internal Config(ConfigFile configFile) : base(PluginInfo.PLUGIN_GUID) | ||||
|         { | ||||
|             DisplayLyrics = configFile.Bind("General", "Display Lyrics", true, | ||||
|                 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()))); | ||||
| 
 | ||||
| #if DEBUG | ||||
|             SetupEntriesForExtrapolation(configFile); | ||||
|             SetupEntriesToSkipWinding(configFile); | ||||
|             SetupEntriesForPaletteOverride(configFile); | ||||
|             SetupEntriesForTimingsOverride(configFile); | ||||
|  | @ -1708,12 +1537,9 @@ 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) | ||||
|  | @ -1721,7 +1547,6 @@ namespace MuzikaGromche | |||
|             // This is basically what ConfigFile.PopulateEntryContainer does | ||||
|             EntryContainer.Add(entryBase.BoxedEntry.ToSyncedEntryIdentifier(), entryBase); | ||||
|         } | ||||
| #endif | ||||
| 
 | ||||
|         public static CanModifyResult CanModifyIfHost() | ||||
|         { | ||||
|  | @ -1757,23 +1582,6 @@ 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, | ||||
|  | @ -1885,7 +1693,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))); | ||||
|                 new ConfigDescription("Duration of fading out", new AcceptableValueRange<float>(0, 100))); | ||||
|             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", "", | ||||
|  | @ -1981,7 +1789,6 @@ namespace MuzikaGromche | |||
|                 } | ||||
|             } | ||||
|         } | ||||
| #endif | ||||
| 
 | ||||
|         private T Default<T>(T options) where T : BaseOptions | ||||
|         { | ||||
|  | @ -2021,30 +1828,15 @@ namespace MuzikaGromche | |||
|             ChooseTrackDeferred(); | ||||
|             foreach (var track in Plugin.Tracks) | ||||
|             { | ||||
|                 track.Weight.SettingChanged += ChooseTrackDeferredDelegate; | ||||
|                 track.Weight.SettingChanged += (_, _) => ChooseTrackDeferred(); | ||||
|             } | ||||
|             Config.SkipExplicitTracks.SettingChanged += ChooseTrackDeferredDelegate; | ||||
|             Config.SkipExplicitTracks.SettingChanged += (_, _) => ChooseTrackDeferred(); | ||||
|             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) | ||||
|  | @ -2176,7 +1968,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(); | ||||
|  | @ -2189,9 +1981,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 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) | ||||
|                 { | ||||
|                     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 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. | ||||
| 
 | ||||
|  | @ -38,5 +38,4 @@ Any player can change their personal preferences locally. | |||
| 
 | ||||
| [`CSync`]: https://thunderstore.io/c/lethal-company/p/Sigurd/CSync/ | ||||
| [`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/ | ||||
|  |  | |||
|  | @ -1,11 +1,12 @@ | |||
| { | ||||
|     "name": "MuzikaGromche", | ||||
|     "version_number": "1337.420.69", | ||||
|     "version_number": "1337.69.420", | ||||
|     "author": "Ratijas", | ||||
|     "description": "Add some content to your inverse teleporter experience on Titan!", | ||||
|     "website_url": "https://git.vilunov.me/ratijas/muzika-gromche", | ||||
|     "dependencies": [ | ||||
|         "BepInEx-BepInExPack-5.4.2100", | ||||
|         "Sigurd-CSync-5.0.1", | ||||
|         "AinaVT-LethalConfig-1.4.6", | ||||
|         "WaterGun-V70PoweredLights_Fix-1.0.0", | ||||
|         "BMX-LobbyCompatibility-1.5.1" | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue